Initialisations and preinstallation
[p5sagit/Oyster.git] / lib / Oyster / Provision / Rackspace.pm
1 package Oyster::Provision::Rackspace;
2 use Moose::Role;
3 use Net::RackSpace::CloudServers;
4 use Net::RackSpace::CloudServers::Server;
5 use MIME::Base64;
6
7 # TODO http://failverse.com/manually-creating-a-cloud-server-from-a-cloud-files-image/
8 # in order to use an already created image to build the server, a la EC2 way
9
10 requires 'config';
11
12 has 'api_username' => ( is => 'ro', isa => 'Str', required => 1, lazy => 1, default => sub {
13     my $self = shift;
14     return $ENV{CLOUDSERVERS_USER} if exists $ENV{CLOUDSERVERS_USER};
15     return $self->config->{api_username}
16         or die "Need api_username or CLOUDSERVERS_USER in environment";
17 });
18
19 has 'api_password' => ( is => 'ro', isa => 'Str', required => 1, lazy => 1, default => sub {
20     my $self = shift;
21     return $ENV{CLOUDSERVERS_KEY} if exists $ENV{CLOUDSERVERS_KEY};
22     return $self->config->{api_password}
23         or die "Need api_password or CLOUDSERVERS_KEY in environment";
24 });
25
26 has '_rs' => ( is => 'rw', isa => 'Net::RackSpace::CloudServers', lazy => 1, default => sub {
27     my $self = shift;
28     my $rs = eval {
29         Net::RackSpace::CloudServers->new(
30             user => $self->api_username,
31             key  => $self->api_password,
32         );
33     };
34     if ( $@ ) {
35         die
36             "Could not instantiate a backend connection to RackSpace CloudServers.\n",
37             "Check the api_username and api_password on the configuration file\n";
38     }
39     $rs;
40 });
41
42 sub create {
43    my $self = shift;
44
45    die "Rackspace Provisioning backend requires a server name\n" if !defined $self->name;
46
47    # Do nothing if the server named $self->name already exists
48    return if scalar grep { $_->name eq $self->name } $self->_rs->get_server();
49
50    # Validate size and image
51    {
52        die "Rackspace Provisioning backend requires a server image\n"  if !defined $self->image;
53        my @allowed_images = $self->_rs->get_image();
54        my $image_id = $self->image;
55        if ( !scalar grep { $_->{id} eq $image_id } @allowed_images ) {
56            die "Rackspace Provisioning backend requires a valid image id\nValid images:\n",
57                (map { sprintf("id %-10s -- %s\n", $_->{id}, $_->{name}) } @allowed_images),
58                "\n";
59        }
60
61        die "Rackspace Provisioning backend requires a server size\n"  if !defined $self->size;
62        my @allowed_flavors = $self->_rs->get_flavor();
63        my $flavor_id = $self->size;
64        if ( !scalar grep { $_->{id} eq $flavor_id } @allowed_flavors ) {
65            die "Rackspace Provisioning backend requires a valid size id\nValid flavors:\n",
66                (map { sprintf("id %-10s -- %s\n", $_->{id}, $_->{name}) } @allowed_flavors),
67                "\n";
68        }
69    }
70
71    # Check the ssh pub key exists and is <10K
72    die "SSH pubkey needs to exist" if !-f $self->pub_ssh;
73    my $pub_ssh = do {
74        local $/=undef;
75        open my $fh, '<', $self->pub_ssh or die "Cannot open ", $self->pub_ssh, ": $!";
76        my $_data = <$fh>;
77        close $fh or die "Cannot close ", $self->pub_ssh, ": $!";
78        $_data;
79    };
80    die "SSH pubkey needs to be < 10KiB" if length $pub_ssh > 10*1024;
81
82    # Build the server
83    my $server = Net::RackSpace::CloudServers::Server->new(
84       cloudservers => $self->_rs,
85       name         => $self->name,
86       flavorid     => $self->size,
87       imageid      => $self->image,
88       personality => [
89            {
90                path     => '/root/.ssh/authorized_keys',
91                contents => encode_base64($pub_ssh),
92            },
93       ],
94    );
95    my $newserver = $server->create_server;
96    warn "Server root password: ", $newserver->adminpass, "\n";
97
98    do {
99       $|=1;
100       my @tmpservers = $self->_rs->get_server_detail();
101       $server = ( grep { $_->name eq $self->name } @tmpservers )[0];
102       print "\rServer status: ", ($server->status || '?'), " progress: ", ($server->progress || '?');
103       if ( ( $server->status // '' ) ne 'ACTIVE' ) {
104         print " sleeping..";
105         sleep 2;
106       }
107    } while ( ( $server->status // '' ) ne 'ACTIVE' );
108
109    warn "Server public IPs are: @{$server->public_address}\n";
110    my $public_ip  = (@{$server->public_address})[0];
111    my $servername = sprintf("oyster-%s", $self->name);
112
113    # Adds the server's name to the user's ~/.ssh/config
114    # using oyster-servername
115    {
116        open my $fh, '>>', "$ENV{HOME}/.ssh/config"
117            or die "Error opening $ENV{HOME}/.ssh/config for appending: $!";
118        my $template = "\nHost %s\n" .
119            "    User root\n" .
120            "    Port 22\n" .
121            "    Compression yes\n" .
122            "    HostName %s\n" .
123            "\n";
124        print $fh sprintf($template, $servername, $public_ip);
125        close $fh or die "Error closing $ENV{HOME}/.ssh/config: $!";
126    }
127
128    # Connect to server and execute installation routines -- unlike EC2 each
129    # server needs instantiated from scratch every time
130    warn "Initializing the server...";
131    $self->initialise();
132
133    warn "Deploying the application...";
134    $self->deploy();
135 }
136
137 sub initialise {
138     my $self = shift;
139     my $servername = sprintf("oyster-%s", $self->name);
140
141     # Adds the server's key to the user's ~/.ssh/authorized_keys
142     # FIXME there must be a better way?!
143     warn "Adding SSH key for $servername to ~/.ssh/authorized_keys\n";
144     qx{/usr/bin/ssh -o StrictHostKeyChecking=no -l root $servername 'echo oyster'};
145
146     # FIXME should call the module which does the installation...
147     warn "Installing wget, lighttpd and git...\n";
148     print qx{/usr/bin/ssh -l root $servername 'LC_ALL=C /usr/bin/apt-get install --yes wget lighttpd git git-core'};
149     print qx{/usr/bin/ssh -l root $servername 'LC_ALL=C /usr/sbin/service lighttpd stop'};
150     warn "Adding user perloyster...\n";
151     print qx{/usr/bin/ssh -l root $servername 'LC_ALL=C /usr/sbin/adduser --disabled-password --gecos "Perl Oyster" perloyster'};
152     warn "Copying keys to ~perloyster...\n";
153     print qx{/usr/bin/ssh -l root $servername 'LC_ALL=C /bin/mkdir ~perloyster/.ssh/'};
154     print qx{/usr/bin/ssh -l root $servername 'LC_ALL=C /bin/cp ~/.ssh/authorized_keys ~perloyster/.ssh/'};
155     print qx{/usr/bin/ssh -l root $servername 'LC_ALL=C /bin/chown --recursive perloyster ~perloyster/.ssh/'};
156     warn "Making perloyster readable...\n";
157     print qx{/usr/bin/ssh -l perloyster $servername 'LC_ALL=C /bin/chmod a+r ~perloyster/'};
158     #warn "Installing cpanminus...\n";
159     #print qx{/usr/bin/ssh -l perloyster $servername 'LC_ALL=C /usr/bin/wget --no-check-certificate http://xrl.us/cpanm ; chmod +x cpanm'};
160     #warn "Installing prerequisites for Oyster::Deploy::Git...\n";
161     #print qx{/usr/bin/ssh -l perloyster $servername 'LC_ALL=C ./cpanm --local-lib=~/perl5 App::cpanminus Dist::Zilla'};
162     warn "Getting and unpacking base system...\n";
163     print qx{/usr/bin/ssh -l perloyster $servername 'LC_ALL=C /usr/bin/wget --no-check-certificate https://darkpan.com/files/oyster-prereqs-20101122-2217.tgz'};
164     print qx{/usr/bin/ssh -l perloyster $servername 'LC_ALL=C /bin/tar xvf oyster-prereqs-20101122-2217.tgz'};
165     print qx{/usr/bin/ssh -l perloyster $servername 'LC_ALL=C /bin/echo export PERL5LIB="/home/perloyster/perl5/lib/perl5:/home/perloyster/perl/lib/perl5/x86_64-linux-gnu-thread-multi" >> ~/.bashrc'};
166     print qx{/usr/bin/ssh -l perloyster $servername 'LC_ALL=C /bin/echo export PATH="/home/perloyster/perl5/bin:\$PATH" >> ~/.bashrc'};
167
168     warn "Pushing and unpacking Oyster::Deploy::Git...\n";
169     print qx{/usr/bin/ssh -l perloyster $servername 'LC_ALL=C /bin/mkdir -p perl5/lib/perl5/Oyster/Deploy'};
170     print qx{/usr/bin/scp lib/Oyster/Deploy/Git.pm perloyster\@$servername:perl5/lib/perl5/Oyster/Deploy/};
171 }
172
173 sub deploy {
174     my $self = shift;
175     my $servername = sprintf("oyster-%s", $self->name);
176     warn "Deploying application to $servername...\n";
177     print qx{/usr/bin/ssh -l perloyster $servername "perl -MOyster::Deploy::Git -le'\$g=Oyster::Deploy::Git->new;\$g->deploy(q,/home/perloyster/oyster,)'"};
178 }
179
180 sub delete {
181    my $self = shift;
182
183    # Die if the server named $self->name already exists
184    my ($server) = grep { $_->name eq $self->name } $self->_rs->get_server();
185    die "No such server: ", $self->name if !$server;
186
187    # Goodbye cruel user!
188    $server->delete_server();
189 }
190
191 sub resize {
192    my $self = shift;
193
194    $self->config();
195 }
196
197 1;
198
199 __END__
200
201 =head1 NAME
202
203 Oyster::Provision::Rackspace -- Provision your Oyster on Rackspace
204
205 =head1 SYNOPSIS
206
207 Use the Rackspace backend on your Oyster configuration file
208
209 =head1 REQUIRED PARAMETERS
210
211 The following are required to instantiate a backend:
212
213 =over
214
215 =item api_username
216
217 The rackspace API username, or C<$ENV{RACKSPACE_USER}> will be used if that is
218 not given
219
220 =item api_password
221
222 This is your rackspace API Key
223
224 The rackspace API key, or C<$ENV{RACKSPACE_KEY}> will be used if that is not
225 given
226
227 =item name
228
229 The name of your new/existing rackspace server.
230
231 =item size
232
233 The size ID of the rackspace server you want to create.
234 Use the following incantation to see them:
235
236     perl -MNet::RackSpace::CloudServers -e'
237         $r=Net::RackSpace::CloudServers->new(
238             user=>$ENV{CLOUDSERVERS_USER},
239             key=>$ENV{CLOUDSERVERS_KEY},
240         );
241         print map
242             { "id $_->{id} ram $_->{ram} disk $_->{disk}\n" }
243             $r->get_flavor_detail
244     '
245     id 1 ram 256 disk 10
246     id 2 ram 512 disk 20
247     id 3 ram 1024 disk 40
248     id 4 ram 2048 disk 80
249     id 5 ram 4096 disk 160
250     id 6 ram 8192 disk 320
251     id 7 ram 15872 disk 620
252
253 =item image
254
255 The image ID of the rackspace server you want to create.
256 Use the following incantation to see them:
257
258     perl -MNet::RackSpace::CloudServers -e'
259         $r=Net::RackSpace::CloudServers->new(
260             user=>$ENV{CLOUDSERVERS_USER},
261             key=>$ENV{CLOUDSERVERS_KEY},
262         );
263         print map
264             { "id $_->{id} name $_->{name}\n" }
265             $r->get_image_detail
266     '
267     id 29 name Windows Server 2003 R2 SP2 x86
268     id 69 name Ubuntu 10.10 (maverick)
269     id 41 name Oracle EL JeOS Release 5 Update 3
270     id 40 name Oracle EL Server Release 5 Update 4
271     id 187811 name CentOS 5.4
272     id 4 name Debian 5.0 (lenny)
273     id 10 name Ubuntu 8.04.2 LTS (hardy)
274     id 23 name Windows Server 2003 R2 SP2 x64
275     id 24 name Windows Server 2008 SP2 x64
276     id 49 name Ubuntu 10.04 LTS (lucid)
277     id 14362 name Ubuntu 9.10 (karmic)
278     id 62 name Red Hat Enterprise Linux 5.5
279     id 53 name Fedora 13
280     id 17 name Fedora 12
281     id 71 name Fedora 14
282     id 31 name Windows Server 2008 SP2 x86
283     id 51 name CentOS 5.5
284     id 14 name Red Hat Enterprise Linux 5.4
285     id 19 name Gentoo 10.1
286     id 28 name Windows Server 2008 R2 x64
287     id 55 name Arch 2010.05
288
289 Oyster only supports Linux images, specifically
290 Ubuntu 10.10 (maverick).
291
292 =item pub_ssh
293
294 The public ssh key you would like copied to the
295 new server's C</root/.ssh/authorized_keys> file
296 to allow you to ssh in the box without providing
297 a root password.
298
299 =back
300
301 =cut