/** * Execute the job. * * @return void */ public function handle() { $this->log = new Logger('LOG'); $this->log->pushHandler(new StreamHandler(storage_path('logs/provision/' . $this->provision->uuid . '.log'), Logger::INFO)); $logProvisionerOutput = new Logger('OUTPUT'); $logProvisionerOutput->pushHandler(new StreamHandler(storage_path('logs/provision/' . $this->provision->uuid . '.output'), Logger::INFO)); $this->log->addInfo("Provisioning started"); $uuid = $this->provision->uuid; $sshKeys = new \App\Fodor\Ssh\Keys($uuid); //TODO: This should be a beanstalk job with AJAX updating //DO ALL THE PROVISIONING HERE - GET GITHUB FODOR.JSON, SSH IN, DO IT ALL, $this->provision->status = 'provision'; $this->provision->save(); $this->log->addInfo("Set status to provision"); $repo = new Repo($this->provision->repo); $branch = $repo->getBranch(); $username = $repo->getUsername(); $client = new \Github\Client(); $client->authenticate(env('GITHUB_API_TOKEN'), false, \Github\Client::AUTH_HTTP_TOKEN); $github = new Github($client, $repo); $json = $github->getFodorJson(); $this->log->addInfo("Fetched fodor.json from GitHub: {$json}"); $fodorJson = new Config($json); try { $fodorJson->valid(); } catch (\Exception $e) { $this->log->addError('Fodor.json invalid'); } $baseScript = \View::make('provision-base.ubuntu-14-04-x64', ['installpath' => $fodorJson->installpath, 'name' => $this->provision->repo, 'domain' => $this->provision->subdomain . '.fodor.xyz', 'ipv4' => $this->provision->ipv4, 'inputs' => $this->inputs])->render(); // Has to be less than 1mb $providedScript = $github->getFileContents($fodorJson->provisioner); if (empty($providedScript)) { $this->log->addError('Provisioner invalid'); } $this->log->addInfo("Fetched provisioner script from GitHub: {$providedScript}"); $remoteProvisionScriptPath = '/tmp/fodor-provision-script-' . $this->provision->uuid; if ($ssh = ssh2_connect($this->provision->ipv4, 22, [], ['disconnect' => [$this, 'tidyUp']])) { $this->log->addInfo("Successfully connected to the server via SSH: {$this->provision->ipv4}"); if (ssh2_auth_pubkey_file($ssh, 'root', storage_path('app/' . $sshKeys->getPublicKeyPath()), storage_path('app/' . $sshKeys->getPrivateKeyPath()))) { $this->log->addInfo("Successfully authenticated"); $this->log->addInfo("Running: /bin/bash '{$remoteProvisionScriptPath}'"); $sftp = ssh2_sftp($ssh); //TODO error check and refactor all of the code we've written so far $stream = fopen("ssh2.sftp://{$sftp}{$remoteProvisionScriptPath}", 'w'); $fullScript = $baseScript . PHP_EOL . $providedScript . PHP_EOL; fwrite($stream, $fullScript); fclose($stream); $this->log->addInfo("Transferred provisioner-combined script"); // TODO: Investigate setting environment variables here instead of with export $stream = ssh2_exec($ssh, '(/bin/bash ' . escapeshellarg($remoteProvisionScriptPath) . ' 2>&1); echo -e "\\n$?"'); stream_set_blocking($stream, true); $lastString = ''; while (($string = fgets($stream)) !== false) { $logProvisionerOutput->addInfo($string); echo $string; $lastString = $string; } $this->log->addInfo('EXIT CODE: ' . $lastString); $this->exitCode = (int) trim($lastString); fclose($stream); } else { $this->log->addError("Failed to authenticate to SSH"); exit(1); } } else { $this->log->addError("Failed to connect to SSH"); $this->release(2); // Delay x seconds to retry as SSH isn't ready yet exit(1); } $this->tidyUp(); }
public function start(Request $request, $repo = false) { try { $repo = new Repo($repo); } catch (\Exception $e) { $request->session()->flash(str_random(4), ['type' => 'danger', 'message' => $e->getMessage()]); return redirect('/?ohno'); } $request->session()->set('intendedRepo', $repo); if ($request->session()->has('digitalocean') === false) { return redirect(url('/do/start')); } $request->session()->forget('intendedRepo'); $provision = new Provision(); $provision->repo = $repo->getName(); // Before it gets contaminated $github = new Github($this->getGithubClient(), $repo); $json = $github->getFodorJson(); if (empty($json)) { $request->session()->flash(str_random(4), ['type' => 'warning', 'message' => 'This repo or repo\'s fodor.json is non-existent']); return redirect(url('/')); } $fodorJson = new Config($json); try { $fodorJson->valid(); } catch (\Exception $e) { $request->session()->flash(str_random(4), ['type' => 'danger', 'message' => $e->getMessage()]); return redirect('/?ohno'); } $fodorJsonUndecoded = $fodorJson->getJson(); // string of json // Has to be less than 1mb $provisioner = $github->getFileContents($fodorJson->provisioner); if (empty($provisioner)) { $request->session()->flash(str_random(4), ['type' => 'warning', 'message' => 'This repo\'s provisioner was invalid or empty']); return redirect(url('/?ohno')); } // We have a valid provisioner $size = '512mb'; // TODO: Config variable for default size $suggestedSize = false; $requiredSize = false; if (array_key_exists('required', $fodorJson->size) === true) { $size = $fodorJson->size['required']; $requiredSize = $size; } // Suggested size overrides the default and required size if (array_key_exists('suggested', $fodorJson->size) === true) { $size = $fodorJson->size['suggested']; $suggestedSize = $size; } if (array_key_exists($size, config('digitalocean.sizes')) === false) { // Invalid size $size = '512mb'; // TODO: Config variable for default size } $isDistroInvalid = !in_array($fodorJson->distro, config('digitalocean.distros')); if (empty($fodorJson->distro) || $isDistroInvalid) { $fodorJson->distro = 'ubuntu-14-04-x64'; } $adapter = new GuzzleHttpAdapter($request->session()->get('digitalocean')['token']); $digitalocean = new DigitalOceanV2($adapter); $keysCached = false; $cacheKey = sha1($request->session()->get('digitalocean')['token'] . '-sshkeys'); $keys = Cache::get($cacheKey, []); if (empty($keys)) { $keysFromDo = $digitalocean->key()->getAll(); if (empty($keysFromDo)) { $request->session()->flash(str_random(4), ['type' => 'danger', 'message' => 'You must have SSH keys attached to your DigitalOcean account to continue - https://cloud.digitalocean.com/settings/security']); return redirect(url('/provision/' . $provision->repo)); } foreach ($keysFromDo as $key) { if (strpos($key->name, 'fodor-') !== 0) { $keys[$key->id] = $key->name; } } if (empty($keys)) { $request->session()->flash(str_random(4), ['type' => 'danger', 'message' => 'You must have SSH keys attached to your DigitalOcean account to continue - https://cloud.digitalocean.com/settings/security']); return redirect(url('/provision/' . $provision->repo)); } Cache::put($cacheKey, $keys, 5); // Cache SSH keys for 5 minutes } else { $keysCached = true; } $requiredMemory = 0; if (!empty($requiredSize)) { $requiredMemory = array_key_exists($requiredSize, config('digitalocean.sizes')) ? config('digitalocean.sizes')[$requiredSize]['memory'] : 0; } $inputs = is_null($fodorJson->inputs) === false ? $fodorJson->inputs : []; array_walk($inputs, function (&$input) { $input['value'] = ''; $input = new Input($input); }); //get account email, and digitalocean_uuid //generate our own uuid //store in DB $provision->uuid = Uuid::uuid4()->toString(); $provision->email = $request->session()->get('digitalocean')['email']; $provision->digitalocean_uuid = $request->session()->get('digitalocean')['uuid']; $provision->size = $size; // Default, can be overriden in next step $provision->distro = $fodorJson->distro; $provision->region = 'xxx'; // Default, can be overriden in next step $provision->datestarted = (new \DateTime('now', new \DateTimeZone('UTC')))->format('c'); $validatingInputs = $request->input('uuid') !== null ? true : false; if ($validatingInputs) { $provision = \App\Provision::where('id', $request->input('provisionid'))->where('uuid', $request->input('uuid'))->first(); /** * @var Input $input */ $invalidInputs = []; foreach ($inputs as $input) { $value = $request->input('inputs')[$input->getName()]; $input->value($value); $valid = $input->validate($value); if ($valid === false) { $invalidInputs[] = ucwords($input->getName()); } } $selectedKeys = $request->input('keys'); $keysChosen = count($selectedKeys) > 0; if ($keysChosen === false) { $request->session()->now(str_random(4), ['type' => 'warning', 'message' => 'You must select an SSH key to add to the server']); } elseif (count($invalidInputs) === 0 && $keysChosen) { return $this->doit($request); } else { $request->session()->now(str_random(4), ['type' => 'warning', 'message' => 'Input value was invalid for ' . implode(', ', $invalidInputs)]); } } else { try { $saved = $provision->save(); } catch (\Exception $e) { $saved = false; } if (empty($saved)) { // Failed to save $request->session()->flash(str_random(4), ['type' => 'danger', 'message' => 'Failed to save the provision data to the database, please destroy your droplet']); return redirect(url('/provision/' . $provision->repo)); } } return view('provision.start', ['repo' => $repo->getRepoName(), 'size' => ['default' => $size, 'suggested' => $suggestedSize, 'required' => $requiredSize], 'requiredMemory' => $requiredMemory, 'description' => $fodorJson->description, 'distro' => $fodorJson->distro, 'keys' => $keys, 'provisionid' => $provision->id, 'provision' => $provision, 'id' => $provision->id, 'uuid' => $provision->uuid, 'inputs' => $inputs, 'keysCached' => $keysCached]); }