├── var └── cache │ └── .gitignore ├── playbooks ├── lemp │ ├── templates │ │ ├── info.php.j2 │ │ └── ubuntu1804.j2 │ └── ubuntu1804.yml └── default │ └── ubuntu1804.yml ├── .gitignore ├── src ├── Exception │ ├── APIException.php │ ├── CommandNotFoundException.php │ ├── MissingArgumentException.php │ └── InvalidArgumentCountException.php ├── Model │ ├── DigitalOcean │ │ ├── Key.php │ │ ├── Image.php │ │ ├── Size.php │ │ ├── Region.php │ │ ├── Droplet.php │ │ └── APIObject.php │ └── Ansible │ │ ├── Host.php │ │ ├── Group.php │ │ └── Inventory.php ├── Provider │ ├── APIClientInterface.php │ ├── RandomNameProvider.php │ ├── APIClientProvider.php │ └── AnsibleProvider.php ├── Core │ ├── Config.php │ ├── FileCache.php │ ├── CommandController.php │ ├── CommandRegistry.php │ └── CLIPrinter.php ├── Command │ ├── HelpController.php │ ├── InventoryController.php │ ├── DeployerController.php │ ├── FetchController.php │ └── DropletController.php ├── Wrapper │ └── Ansible.php ├── Deployer.php ├── Dolphin.php └── DigitalOcean.php ├── dolphin.php ├── hosts.php ├── composer.json ├── LICENSE ├── config_sample.php └── README.md /var/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /playbooks/lemp/templates/info.php.j2: -------------------------------------------------------------------------------- 1 | runCommand($argc, $argv); 17 | -------------------------------------------------------------------------------- /hosts.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | runCommand(3, [ 'dolphin', 'inventory', 'json']); -------------------------------------------------------------------------------- /src/Provider/APIClientInterface.php: -------------------------------------------------------------------------------- 1 | config = $config; 20 | } 21 | 22 | public function __get($name) 23 | { 24 | return isset($this->config[$name]) ? $this->config[$name] : null; 25 | } 26 | 27 | /** 28 | * @param string $name 29 | * @param string $value 30 | */ 31 | public function __set($name, $value) 32 | { 33 | $this->config[$name] = $value; 34 | } 35 | } -------------------------------------------------------------------------------- /src/Model/DigitalOcean/APIObject.php: -------------------------------------------------------------------------------- 1 | values = $values; 21 | } 22 | 23 | /** 24 | * @param string $name Key 25 | * @param string $value Value 26 | */ 27 | function __set($name, $value) 28 | { 29 | $this->values[$name] = $value; 30 | } 31 | 32 | /** 33 | * @param string $name 34 | * @return string|null 35 | */ 36 | function __get($name) 37 | { 38 | return isset($this->values[$name]) ? $this->values[$name] : null; 39 | } 40 | } -------------------------------------------------------------------------------- /src/Provider/RandomNameProvider.php: -------------------------------------------------------------------------------- 1 | getPrinter()->printUsage(); 16 | $this->printCheatSheet(); 17 | } 18 | 19 | /** 20 | * Print commands usage 21 | */ 22 | public function printCheatSheet() 23 | { 24 | $printer = $this->getPrinter(); 25 | 26 | $printer->newline(); 27 | $printer->out("Commands & Sub-commands\n\n", 'info'); 28 | 29 | $registered_commands = $this->getDolphin()->getCommandRegistry()->getRegisteredCommands(); 30 | 31 | foreach ($registered_commands as $namespace => $commands) { 32 | $printer->out("$namespace", "info_alt"); 33 | $printer->newline(); 34 | 35 | foreach ($commands as $command => $callback) { 36 | $printer->out(" + $command", "default"); 37 | $printer->newline(); 38 | } 39 | 40 | $printer->newline(); 41 | } 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /config_sample.php: -------------------------------------------------------------------------------- 1 | 'default', 10 | 11 | // DigitalOcean API Token 12 | 'DO_API_TOKEN' => 'YOUR_DIGITALOCEAN_API_TOKEN', 13 | 14 | //Default Ansible server group 15 | 'DEFAULT_SERVER_GROUP' => 'servers', 16 | 17 | //Cache location relative to doc root */ 18 | 'CACHE_DIR' => 'var/cache', 19 | 20 | //Cache expiry time in minutes 21 | 'CACHE_EXPIRY' => 60, 22 | 23 | //Default Droplet Settings 24 | 'DO' => [ 25 | 'D_REGION' => 'nyc3', 26 | 'D_IMAGE' => 'ubuntu-18-04-x64', 27 | 'D_SIZE' => 's-1vcpu-1gb', 28 | 'D_TAGS' => [ 29 | 'dolphin' 30 | ], 31 | 32 | // Optional - SSH key(s) to be included for the root user in new droplets. 33 | // Uncomment and add your own key(s) - ID or Fingerprint 34 | // You can list your registered keys with: ./dolphin fetch keys 35 | //'D_SSH_KEYS' => [ 36 | // 'YOUR_SSH_KEY_ID_OR_FINGERPRINT' 37 | //], 38 | ], 39 | 40 | //Default Ansible Settings 41 | 'ANSIBLE_USER' => 'sammy', 42 | 'PLAYBOOKS_DIR' => 'playbooks', 43 | 44 | ]; -------------------------------------------------------------------------------- /src/Wrapper/Ansible.php: -------------------------------------------------------------------------------- 1 | name = $name; 28 | $this->ip = $ip; 29 | $this->tags = $tags; 30 | } 31 | 32 | /** 33 | * @return string 34 | */ 35 | public function getName() 36 | { 37 | return $this->name; 38 | } 39 | 40 | /** 41 | * @param string $name 42 | */ 43 | public function setName($name) 44 | { 45 | $this->name = $name; 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function getIp() 52 | { 53 | return $this->ip; 54 | } 55 | 56 | /** 57 | * @param string $ip 58 | */ 59 | public function setIp($ip) 60 | { 61 | $this->ip = $ip; 62 | } 63 | 64 | /** 65 | * @return string 66 | */ 67 | public function getTags() 68 | { 69 | return $this->tags; 70 | } 71 | 72 | /** 73 | * @param string $tags 74 | */ 75 | public function setTags($tags) 76 | { 77 | $this->tags = $tags; 78 | } 79 | 80 | public function toInventory() 81 | { 82 | return sprintf("%s ansible_host=%s\n", $this->getName(), $this->getIp()); 83 | } 84 | 85 | public function __toString() 86 | { 87 | return $this->getIp(); 88 | } 89 | } -------------------------------------------------------------------------------- /src/Model/Ansible/Group.php: -------------------------------------------------------------------------------- 1 | name = $name; 24 | 25 | foreach ($hosts as $host) { 26 | $this->addHost($host); 27 | } 28 | } 29 | 30 | /** 31 | * @param Host $host 32 | */ 33 | public function addHost(Host $host) 34 | { 35 | $this->hosts[] = $host; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getName() 42 | { 43 | return $this->name; 44 | } 45 | 46 | /** 47 | * @param string $name 48 | */ 49 | public function setName($name) 50 | { 51 | $this->name = $name; 52 | } 53 | 54 | /** 55 | * @return Host[] 56 | */ 57 | public function getHosts() 58 | { 59 | return $this->hosts; 60 | } 61 | 62 | /** 63 | * @param Host[] $hosts 64 | */ 65 | public function setHosts(array $hosts) 66 | { 67 | foreach ($hosts as $host) { 68 | $this->addHost($host); 69 | } 70 | } 71 | 72 | public function toInventory() 73 | { 74 | return sprintf("[%s]\n", $this->getName()); 75 | } 76 | 77 | public function getInventoryHosts() 78 | { 79 | $hosts = []; 80 | 81 | /** @var Host $host */ 82 | foreach ($this->getHosts() as $host) { 83 | $hosts[] = $host->getIp(); 84 | } 85 | 86 | return $hosts; 87 | } 88 | 89 | } -------------------------------------------------------------------------------- /src/Core/FileCache.php: -------------------------------------------------------------------------------- 1 | cache_dir = $cache_dir; 22 | $this->cache_expiry = $cache_expiry; 23 | } 24 | 25 | /** 26 | * @param string $unique_identifier 27 | * @return string 28 | */ 29 | public function getCacheFile($unique_identifier) 30 | { 31 | return $this->cache_dir . '/' . md5($unique_identifier) . '.json'; 32 | } 33 | 34 | /** 35 | * @param string $unique_identifier 36 | * @return null|string 37 | */ 38 | public function getCached($unique_identifier) 39 | { 40 | $cache_file = $this->getCacheFile($unique_identifier); 41 | 42 | if (is_file($cache_file)) { 43 | return file_get_contents($cache_file); 44 | } 45 | 46 | return null; 47 | } 48 | 49 | /** 50 | * @param string $unique_identifier 51 | * @return null|string 52 | */ 53 | public function getCachedUnlessExpired($unique_identifier) 54 | { 55 | $cache_file = $this->getCacheFile($unique_identifier); 56 | 57 | // is it still valid? 58 | if (is_file($cache_file) && (time() - filemtime($cache_file) < 60*$this->cache_expiry)) { 59 | return file_get_contents($cache_file); 60 | } 61 | 62 | return null; 63 | } 64 | 65 | /** 66 | * @param string $content 67 | * @param string $unique_identifier 68 | */ 69 | public function save($content, $unique_identifier) 70 | { 71 | $cache_file = $this->getCacheFile($unique_identifier); 72 | 73 | file_put_contents($cache_file, $content); 74 | } 75 | } -------------------------------------------------------------------------------- /playbooks/default/ubuntu1804.yml: -------------------------------------------------------------------------------- 1 | ########################################################################################################### 2 | # Playbook: Initial Server Setup 3 | # Based on: https://www.digitalocean.com/community/tutorials/initial-server-setup-with-ubuntu-18-04 4 | ################################################################################################################### 5 | 6 | --- 7 | - hosts: all 8 | remote_user: root 9 | gather_facts: false 10 | vars: 11 | create_user: sammy 12 | copy_local_key: "{{ lookup('file', lookup('env','HOME') + '/.ssh/id_rsa.pub') }}" 13 | sys_packages: [ 'curl', 'vim', 'git', 'ufw'] 14 | 15 | tasks: 16 | - name: Make sure we have a 'wheel' group 17 | group: 18 | name: wheel 19 | state: present 20 | 21 | - name: Allow 'wheel' group to have passwordless sudo 22 | lineinfile: 23 | path: /etc/sudoers 24 | state: present 25 | regexp: '^%wheel' 26 | line: '%wheel ALL=(ALL) NOPASSWD: ALL' 27 | validate: '/usr/sbin/visudo -cf %s' 28 | 29 | - name: Create a new regular user with sudo privileges 30 | user: 31 | name: "{{ create_user }}" 32 | state: present 33 | groups: wheel 34 | append: true 35 | create_home: true 36 | shell: /bin/bash 37 | 38 | - name: Set authorized key for remote user 39 | authorized_key: 40 | user: "{{ create_user }}" 41 | state: present 42 | key: "{{ copy_local_key }}" 43 | 44 | - name: Disable password authentication for root 45 | lineinfile: 46 | path: /etc/ssh/sshd_config 47 | state: present 48 | regexp: '^#?PermitRootLogin' 49 | line: 'PermitRootLogin prohibit-password' 50 | 51 | - name: Update apt 52 | apt: update_cache=yes 53 | 54 | - name: Install required system packages 55 | apt: name={{ sys_packages }} state=latest 56 | 57 | - name: UFW - Allow SSH connections 58 | ufw: 59 | rule: allow 60 | name: OpenSSH 61 | 62 | - name: UFW - Deny all other incoming traffic by default 63 | ufw: 64 | state: enabled 65 | policy: deny 66 | direction: incoming 67 | -------------------------------------------------------------------------------- /src/Command/InventoryController.php: -------------------------------------------------------------------------------- 1 | getInventory(); 23 | 24 | if ($inventory === null) { 25 | $this->getPrinter()->error("ERROR: unable to create inventory."); 26 | exit; 27 | } 28 | 29 | echo $inventory->output(); 30 | } 31 | 32 | /** 33 | * Outputs inventory in JSON format 34 | */ 35 | public function dynamicInventory() 36 | { 37 | $inventory = $this->getInventory(); 38 | 39 | if ($inventory === null) { 40 | $this->getPrinter()->error("ERROR: unable to create inventory."); 41 | } 42 | 43 | print $inventory->getJson(); 44 | } 45 | 46 | public function getCommandMap() 47 | { 48 | return [ 49 | 'json' => 'dynamicInventory', 50 | 'ini' => 'outputInventory', 51 | ]; 52 | } 53 | 54 | public function defaultCommand() 55 | { 56 | $this->outputInventory(); 57 | } 58 | 59 | /** 60 | * @return Inventory|null 61 | * @throws \Dolphin\Exception\APIException 62 | */ 63 | protected function getInventory() 64 | { 65 | $droplets = $this->dolphin->getDO()->getDroplets(); 66 | 67 | if ($droplets !== null) { 68 | 69 | $hosts = []; 70 | foreach ($droplets as $droplet_info) { 71 | $droplet = new Droplet($droplet_info); 72 | 73 | $hosts[] = new Host($droplet->name, $droplet->networks['v4'][0]['ip_address'], $droplet->tags); 74 | } 75 | 76 | $groups[] = new Group($this->getConfig('DEFAULT_SERVER_GROUP'), $hosts); 77 | 78 | return new Inventory($groups); 79 | } 80 | 81 | return null; 82 | } 83 | } -------------------------------------------------------------------------------- /src/Provider/APIClientProvider.php: -------------------------------------------------------------------------------- 1 | $headers, 24 | CURLOPT_RETURNTRANSFER => true, 25 | CURLOPT_URL => $endpoint, 26 | CURLINFO_HEADER_OUT => true 27 | ]); 28 | 29 | return $this->getQueryResponse($curl); 30 | } 31 | 32 | /** 33 | * Makes a POST query 34 | * @param $endpoint 35 | * @param array $params 36 | * @param array $headers 37 | * @return array 38 | */ 39 | public function post($endpoint, array $params, $headers = []) 40 | { 41 | $curl = curl_init(); 42 | 43 | curl_setopt_array($curl, [ 44 | CURLOPT_HTTPHEADER => $headers, 45 | CURLOPT_RETURNTRANSFER => true, 46 | CURLOPT_POST => true, 47 | CURLOPT_POSTFIELDS => json_encode($params), 48 | CURLOPT_URL => $endpoint, 49 | #CURLINFO_HEADER_OUT => true, 50 | CURLOPT_TIMEOUT => 120, 51 | ]); 52 | 53 | return $this->getQueryResponse($curl); 54 | } 55 | 56 | /** 57 | * Makes a DELETE query 58 | * @param $endpoint 59 | * @param array $headers 60 | * @return array 61 | */ 62 | public function delete($endpoint, $headers = []) 63 | { 64 | $curl = curl_init(); 65 | 66 | curl_setopt_array($curl, [ 67 | CURLOPT_HTTPHEADER => $headers, 68 | CURLOPT_RETURNTRANSFER => true, 69 | CURLOPT_CUSTOMREQUEST => "DELETE", 70 | CURLOPT_URL => $endpoint, 71 | #CURLINFO_HEADER_OUT => true, 72 | ]); 73 | 74 | return $this->getQueryResponse($curl); 75 | } 76 | 77 | /** 78 | * Exec curl and get response 79 | * @param $curl 80 | * @return array 81 | */ 82 | protected function getQueryResponse($curl) 83 | { 84 | $response = curl_exec($curl); 85 | $response_code = curl_getinfo($curl, CURLINFO_HTTP_CODE); 86 | 87 | curl_close($curl); 88 | 89 | return [ 'code' => $response_code, 'body' => $response ]; 90 | } 91 | 92 | } -------------------------------------------------------------------------------- /src/Provider/AnsibleProvider.php: -------------------------------------------------------------------------------- 1 | inventory_path = $inventory_path; 29 | $this->playbooks_folder = $playbooks_folder; 30 | } 31 | 32 | /** 33 | * @return mixed 34 | */ 35 | public function getRemoteUser() 36 | { 37 | return $this->remote_user; 38 | } 39 | 40 | /** 41 | * @param mixed $remote_user 42 | */ 43 | public function setRemoteUser($remote_user) 44 | { 45 | $this->remote_user = $remote_user; 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function getPlaybooksFolder() 52 | { 53 | return $this->playbooks_folder; 54 | } 55 | 56 | /** 57 | * @param string $playbooks_folder 58 | */ 59 | public function setPlaybooksFolder($playbooks_folder) 60 | { 61 | $this->playbooks_folder = $playbooks_folder; 62 | } 63 | 64 | /** 65 | * @return mixed 66 | */ 67 | public function getInventoryPath() 68 | { 69 | return $this->inventory_path; 70 | } 71 | 72 | /** 73 | * @param mixed $inventory_path 74 | */ 75 | public function setInventoryPath($inventory_path) 76 | { 77 | $this->inventory_path = $inventory_path; 78 | } 79 | 80 | /** 81 | * Prints Ansible version. 82 | */ 83 | public function getVersion() 84 | { 85 | Ansible::version(); 86 | } 87 | 88 | /** 89 | * Pings a host. 90 | * @param $target 91 | */ 92 | public function ping($target) 93 | { 94 | $connection_options = []; 95 | 96 | if (!empty($this->remote_user)) { 97 | $connection_options['ansible_user'] = $this->remote_user; 98 | } 99 | 100 | Ansible::ping($target, $this->inventory_path, $connection_options); 101 | } 102 | 103 | /** 104 | * Runs a playbook. 105 | * @param $playbook 106 | * @param $target 107 | */ 108 | public function play($playbook, $target) 109 | { 110 | $connection_options = []; 111 | 112 | if (!empty($this->remote_user)) { 113 | $connection_options['ansible_user'] = $this->remote_user; 114 | } 115 | 116 | Ansible::play($playbook, $target, $this->inventory_path, $connection_options); 117 | } 118 | } -------------------------------------------------------------------------------- /src/Deployer.php: -------------------------------------------------------------------------------- 1 | ansible_provider = new AnsibleProvider($inventory_path, $playbooks_folder); 25 | } 26 | 27 | /** 28 | * Sets the remote user 29 | * @param $user 30 | */ 31 | public function setAnsibleUser($user) 32 | { 33 | $this->ansible_provider->setRemoteUser($user); 34 | } 35 | 36 | /** 37 | * Runs a Playbook / Deploy Script 38 | * @param $deploy 39 | * @param $system 40 | * @param $target 41 | */ 42 | public function runDeploy($deploy, $system, $target) 43 | { 44 | if ($this->playbookExists($deploy, $system)) { 45 | $playbook_file = $this->getPlaybookPath($deploy, $system); 46 | $this->ansible_provider->play($playbook_file, $target); 47 | } 48 | } 49 | 50 | /** 51 | * Pings a host 52 | * @param $target 53 | */ 54 | public function ping($target) 55 | { 56 | $this->ansible_provider->ping($target); 57 | } 58 | 59 | /** 60 | * @return array 61 | */ 62 | public function getScripts() 63 | { 64 | $scripts = []; 65 | 66 | foreach (glob($this->getPlaybooksFolder() . '/*') as $deploy) { 67 | 68 | $name = basename($deploy); 69 | 70 | foreach (glob($deploy . '/*.yml') as $playbook) { 71 | $file = explode('.', basename($playbook)); 72 | $scripts[] = ['name' => $name, 'system' => $file[0]]; 73 | } 74 | 75 | } 76 | 77 | return $scripts; 78 | } 79 | 80 | /** 81 | * @param $deploy 82 | * @param $system 83 | * @return bool 84 | */ 85 | public function playbookExists($deploy, $system) 86 | { 87 | return is_file($this->getPlaybookPath($deploy, $system)); 88 | } 89 | 90 | /** 91 | * @param $deploy 92 | * @param $system 93 | * @return string 94 | */ 95 | public function getPlaybookPath($deploy, $system) 96 | { 97 | return $this->getPlaybooksFolder() . '/' . $deploy . '/' . $system . '.yml'; 98 | } 99 | 100 | /** 101 | * @return string 102 | */ 103 | public function getPlaybooksFolder() 104 | { 105 | return $this->ansible_provider->getPlaybooksFolder(); 106 | } 107 | 108 | /** 109 | * Shows the current Ansible version on the system 110 | */ 111 | public function showAnsibleVersion() 112 | { 113 | $this->ansible_provider->getVersion(); 114 | } 115 | } -------------------------------------------------------------------------------- /src/Model/Ansible/Inventory.php: -------------------------------------------------------------------------------- 1 | addGroup($group); 21 | } 22 | } 23 | 24 | /** 25 | * @param Group $group 26 | */ 27 | public function addGroup(Group $group) 28 | { 29 | $this->groups[] = $group; 30 | } 31 | 32 | /** 33 | * @return array 34 | */ 35 | public function getGroups() 36 | { 37 | return $this->groups; 38 | } 39 | 40 | /** 41 | * @param array $groups 42 | */ 43 | public function setGroups(array $groups) 44 | { 45 | foreach ($groups as $group) { 46 | $this->addGroup($group); 47 | } 48 | } 49 | 50 | public function getJson() 51 | { 52 | $inventory = []; 53 | $groups = [ 'ungrouped' ]; 54 | $hostvars = []; 55 | $hosts = []; 56 | $all = []; 57 | 58 | /** @var Group $group */ 59 | foreach ($this->getGroups() as $group) { 60 | 61 | $groups[] = $group->getName(); 62 | /** @var Host $host */ 63 | foreach ($group->getHosts() as $host) { 64 | $hostvars[$host->getName()] = [ 65 | 'ansible_host' => $host->getIp(), 66 | 'ansible_python_interpreter' => "/usr/bin/python3" 67 | ]; 68 | 69 | $hosts[] = $host->getName(); 70 | } 71 | 72 | $inventory[$group->getName()] = [ 73 | 'hosts' => $hosts, 74 | ]; 75 | 76 | $hosts = []; 77 | } 78 | 79 | $inventory['all'] = [ 80 | "children" => $groups 81 | ]; 82 | 83 | $inventory['_meta'] = [ 84 | 'hostvars' => $hostvars, 85 | ]; 86 | 87 | return json_encode($inventory, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 88 | } 89 | 90 | /** 91 | * @return string 92 | */ 93 | public function output() 94 | { 95 | $inventory = ""; 96 | 97 | /** @var Group $group */ 98 | foreach ($this->getGroups() as $group) { 99 | $inventory .= $group->toInventory(); 100 | 101 | /** @var Host $host */ 102 | foreach ($group->getHosts() as $host) { 103 | $inventory .= $host->toInventory(); 104 | } 105 | } 106 | 107 | /** Setting Python 3 for all hosts */ 108 | $inventory .= sprintf("\n[all:vars]\nansible_python_interpreter=/usr/bin/python3\n"); 109 | 110 | return $inventory; 111 | } 112 | 113 | /** 114 | * @return string 115 | */ 116 | public function __toString() 117 | { 118 | return $this->output(); 119 | } 120 | } -------------------------------------------------------------------------------- /src/Core/CommandController.php: -------------------------------------------------------------------------------- 1 | dolphin = $dolphin; 26 | } 27 | 28 | /** 29 | * @return array 30 | */ 31 | public function getParameters() 32 | { 33 | return $this->parameters; 34 | } 35 | 36 | /** 37 | * @param array $arguments 38 | */ 39 | public function setParameters(array $arguments) 40 | { 41 | $this->parameters = $this->parseArgs($arguments); 42 | } 43 | 44 | /** 45 | * @param $string 46 | * @param string $style 47 | */ 48 | public function output($string, $style = "default") 49 | { 50 | $this->dolphin->getPrinter()->out($string, $style); 51 | } 52 | 53 | /** 54 | * @param $name 55 | * @return mixed|null 56 | */ 57 | public function getConfig($name) 58 | { 59 | return $this->dolphin->getConfig()->$name; 60 | } 61 | 62 | /** 63 | * @return CLIPrinter 64 | */ 65 | public function getPrinter() 66 | { 67 | return $this->dolphin->getPrinter(); 68 | } 69 | 70 | /** 71 | * Executed once when the Controller is created 72 | * @return null 73 | */ 74 | public function setup() 75 | { 76 | return null; 77 | } 78 | 79 | /** 80 | * @return Dolphin 81 | */ 82 | protected function getDolphin() 83 | { 84 | return $this->dolphin; 85 | } 86 | 87 | /** 88 | * Should return the command map for a command controller, in this format: 89 | * [ 90 | * 'command1' => 'methodName1', 91 | * 'command2' => 'methodName2', 92 | * ... 93 | * ] 94 | * 95 | * @return array 96 | */ 97 | public function getCommandMap() 98 | { 99 | return []; 100 | } 101 | 102 | /** 103 | * This is executed when no additional subcommands or parameters are passed along to a command. 104 | * ex.: ./dolphin help 105 | * @return mixed 106 | */ 107 | public abstract function defaultCommand(); 108 | 109 | /** 110 | * Parse command arguments 111 | * @param array $arguments 112 | * @return array 113 | */ 114 | public function parseArgs(array $arguments) 115 | { 116 | $params = []; 117 | 118 | foreach ($arguments as $argument) { 119 | $tuple = explode("=", $argument); 120 | $params[$tuple[0]] = isset($tuple[1]) ? $tuple[1] : null; 121 | } 122 | 123 | return $params; 124 | } 125 | 126 | public function flagExists($flag) 127 | { 128 | return array_key_exists($flag, $this->getParameters()); 129 | } 130 | } -------------------------------------------------------------------------------- /playbooks/lemp/ubuntu1804.yml: -------------------------------------------------------------------------------- 1 | ############################################################################################################################# 2 | # Playbook: LEMP (Nginx + PHP + MySQL) on Ubuntu 18.04 3 | # Based on: https://www.digitalocean.com/community/tutorials/how-to-install-linux-nginx-mysql-php-lemp-stack-ubuntu-18-04 4 | ############################################################################################################################# 5 | 6 | - hosts: all 7 | remote_user: sammy 8 | become: true 9 | 10 | ################################################### 11 | # VARS section - update these variables accordingly 12 | ################################################### 13 | vars: 14 | mysql_root_password: MYSQL_ROOT_PASSWORD 15 | http_port: 80 16 | http_host: "{{ ansible_facts.eth0.ipv4.address }}" 17 | http_conf: example.com 18 | 19 | ######## 20 | # Tasks 21 | ######## 22 | 23 | tasks: 24 | - name: Install Prerequisites 25 | apt: name={{ item }} update_cache=yes state=latest force_apt_get=yes 26 | loop: [ 'aptitude'] 27 | 28 | - name: Install LEMP Packages 29 | apt: name={{ item }} update_cache=yes state=latest 30 | loop: [ 'nginx', 'mysql-server', 'python3-pymysql', 'php-fpm', 'php-mysql'] 31 | 32 | # Nginx Configuration 33 | 34 | - name: Sets Nginx conf file 35 | template: 36 | src: templates/ubuntu1804.j2 37 | dest: "/etc/nginx/sites-available/{{ http_conf }}" 38 | 39 | - name: Enables new site 40 | file: 41 | src: "/etc/nginx/sites-available/{{ http_conf }}" 42 | dest: "/etc/nginx/sites-enabled/{{ http_conf }}" 43 | state: link 44 | notify: Reload Nginx 45 | 46 | - name: Removes "default" site 47 | file: 48 | path: "/etc/nginx/sites-enabled/default" 49 | state: absent 50 | notify: Reload Nginx 51 | 52 | # MySQL Configuration 53 | 54 | - name: Sets the root password 55 | mysql_user: 56 | name: root 57 | password: "{{ mysql_root_password }}" 58 | login_unix_socket: /var/run/mysqld/mysqld.sock 59 | 60 | - name: Removes all anonymous user accounts 61 | mysql_user: 62 | name: '' 63 | host_all: yes 64 | state: absent 65 | login_user: root 66 | login_password: "{{ mysql_root_password }}" 67 | 68 | - name: Removes the MySQL test database 69 | mysql_db: 70 | name: test 71 | state: absent 72 | login_user: root 73 | login_password: "{{ mysql_root_password }}" 74 | 75 | # UFW Configuration 76 | 77 | - name: "UFW - Allow HTTP on port {{ http_port }}" 78 | ufw: 79 | rule: allow 80 | port: "{{ http_port }}" 81 | proto: tcp 82 | 83 | # Sets Up PHP Info Page 84 | 85 | - name: Sets Up PHP Info Page 86 | template: 87 | src: templates/info.php.j2 88 | dest: "/var/www/html/info.php" 89 | 90 | # Handlers 91 | 92 | handlers: 93 | - name: Reload Nginx 94 | service: 95 | name: nginx 96 | state: reloaded 97 | 98 | - name: Restart Nginx 99 | service: 100 | name: nginx 101 | state: restarted 102 | -------------------------------------------------------------------------------- /src/Command/DeployerController.php: -------------------------------------------------------------------------------- 1 | getPrinter()->info("Deploy Scripts Currently Available"); 19 | $deployer = $this->getDolphin()->getDeployer(); 20 | $scripts = $deployer->getScripts(); 21 | 22 | foreach ($scripts as $deploy) { 23 | $this->getPrinter()->newline(); 24 | $this->getPrinter()->out($deploy['name'], "info"); 25 | $this->getPrinter()->out(' for ', "default"); 26 | $this->getPrinter()->out($deploy['system'], "info"); 27 | } 28 | 29 | $this->getPrinter()->newline(); 30 | $this->getPrinter()->newline(); 31 | 32 | $this->getPrinter()->out("You can run a script with: ./dolphin deployer run [script] on [droplet-name]"); 33 | $this->getPrinter()->newline(); 34 | } 35 | 36 | 37 | /** 38 | * dolphin deployer ping droplet-name 39 | * @param array $arguments 40 | */ 41 | public function ping(array $arguments) 42 | { 43 | $droplet = $arguments[0]; 44 | 45 | if (!$droplet) { 46 | $this->getPrinter()->error("You must provide the droplet name or IP address."); 47 | exit; 48 | } 49 | 50 | $deployer = $this->getDolphin()->getDeployer(); 51 | 52 | $params = $this->parseArgs($arguments); 53 | if (isset($params['user'])) { 54 | $deployer->setAnsibleUser($params['user']); 55 | } 56 | 57 | $deployer->ping($droplet); 58 | } 59 | 60 | /** 61 | * dolphin deployer run lemp on droplet-name 62 | * @param array $arguments 63 | */ 64 | public function runDeploy(array $arguments) 65 | { 66 | $deployer = $this->getDolphin()->getDeployer(); 67 | 68 | $deploy = $arguments[0]; 69 | $system = "ubuntu1804"; 70 | $target = $arguments[2]; 71 | 72 | if (!$deployer->playbookExists($deploy, $system)) { 73 | $this->getPrinter()->error('The specified deploy is invalid or not available for the requested system.'); 74 | } 75 | 76 | $params = $this->parseArgs($arguments); 77 | if (isset($params['user'])) { 78 | $deployer->setAnsibleUser($params['user']); 79 | } 80 | 81 | $deployer->runDeploy($deploy, $system, $target); 82 | } 83 | 84 | public function getCommandMap() 85 | { 86 | return [ 87 | 'info' => 'infoCommand', 88 | 'list' => 'listScripts', 89 | 'run' => 'runDeploy', 90 | 'ping' => 'ping', 91 | ]; 92 | } 93 | 94 | public function infoCommand() 95 | { 96 | $deployer = $this->getDolphin()->getDeployer(); 97 | 98 | $this->getPrinter()->info('Ansible Version:'); 99 | $deployer->showAnsibleVersion(); 100 | 101 | $this->getPrinter()->info('Playbooks Dir:'); 102 | $this->getPrinter()->out($deployer->getPlaybooksFolder(), 'info_alt'); 103 | $this->getPrinter()->newline(); 104 | } 105 | 106 | public function defaultCommand() 107 | { 108 | $this->infoCommand(); 109 | } 110 | } -------------------------------------------------------------------------------- /src/Core/CommandRegistry.php: -------------------------------------------------------------------------------- 1 | dolphin = $dolphin; 24 | } 25 | 26 | /** 27 | * @param string $namespace Command Namespace 28 | * @param string $command_name Command name 29 | * @param array $arguments Command parameters 30 | * @return mixed 31 | * @throws CommandNotFoundException 32 | */ 33 | public function runCommand($namespace, $command_name, array $arguments) 34 | { 35 | $command = $this->getCommand($namespace, $command_name); 36 | 37 | if ($command == null) { 38 | throw new CommandNotFoundException("Command not found."); 39 | } 40 | 41 | /** @var CommandController $controller */ 42 | $controller = $this->getController($namespace); 43 | $controller->setParameters($arguments); 44 | 45 | return $controller->$command($arguments); 46 | } 47 | 48 | /** 49 | * @param string $autoload_dir Directory for autoloading Command Namespaces 50 | */ 51 | public function autoloadNamespaces($autoload_dir) 52 | { 53 | foreach (glob($autoload_dir . '/*Controller.php') as $filepath) { 54 | $this->loadController($filepath); 55 | } 56 | } 57 | 58 | public function getRegisteredCommands() 59 | { 60 | return $this->command_map; 61 | } 62 | 63 | /** 64 | * @param string $namespace 65 | * @return CommandController 66 | */ 67 | public function getController($namespace) 68 | { 69 | return key_exists($namespace, $this->controllers) ? $this->controllers[$namespace] : null; 70 | } 71 | 72 | /** 73 | * Registers a controller 74 | * @param $filepath 75 | */ 76 | protected function loadController($filepath) 77 | { 78 | $fileinfo = pathinfo($filepath); 79 | 80 | $fq_class_name = "Dolphin\\Command\\" . $fileinfo['filename']; 81 | 82 | $namespace = strtolower(str_replace('Controller', '', $fileinfo['filename'])); 83 | 84 | /** @var CommandController $controller */ 85 | $controller = new $fq_class_name($this->getDolphin()); 86 | $controller->setup(); 87 | 88 | $this->controllers[$namespace] = $controller; 89 | $this->registerCommands($namespace, $controller->getCommandMap()); 90 | } 91 | 92 | /** 93 | * Registers commands from a namespace 94 | * @param string $namespace Command namespace 95 | * @param array $commands 96 | */ 97 | protected function registerCommands($namespace, array $commands) 98 | { 99 | foreach ($commands as $command => $callback) { 100 | $this->registerCommand($namespace, $command, $callback); 101 | } 102 | } 103 | 104 | /** 105 | * Registers a command in a namespace 106 | * @param string $namespace Command Namespace 107 | * @param string $command Command name 108 | * @param string $callback Callback Controller Method 109 | */ 110 | protected function registerCommand($namespace, $command, $callback) 111 | { 112 | $this->command_map[$namespace][$command] = $callback; 113 | } 114 | 115 | /** 116 | * @param string $namespace Command namespace to search for 117 | * @param string $command Name of the command 118 | * @return string|null Returns the method name for that call, or null if command cannot be found 119 | */ 120 | protected function getCommand($namespace, $command) 121 | { 122 | if (key_exists($namespace, $this->command_map)) { 123 | return isset($this->command_map[$namespace][$command]) ? $this->command_map[$namespace][$command] : null; 124 | } 125 | 126 | return null; 127 | } 128 | 129 | /** 130 | * @return Dolphin 131 | */ 132 | protected function getDolphin() 133 | { 134 | return $this->dolphin; 135 | } 136 | } -------------------------------------------------------------------------------- /src/Dolphin.php: -------------------------------------------------------------------------------- 1 | setConfig($config); 44 | $this->command_registry = new CommandRegistry($this); 45 | $this->command_registry->autoloadNamespaces(__DIR__ . '/Command'); 46 | 47 | // Simple Cache 48 | $cache_dir = __DIR__ . '/../' . $this->getConfig()->CACHE_DIR; 49 | $this->cache = new FileCache($cache_dir, $this->getConfig()->CACHE_EXPIRY); 50 | 51 | // CLI printer 52 | $this->printer = new CLIPrinter($this->getConfig()->THEME); 53 | 54 | // DO API 55 | $this->do = new DigitalOcean($this->getConfig()->DO_API_TOKEN, new APIClientProvider(), $this->cache, $this->getConfig()->DO); 56 | 57 | // Deployer 58 | $this->deployer = new Deployer(__DIR__ . '/../hosts.php', __DIR__ . '/../' . $this->getConfig()->PLAYBOOKS_DIR); 59 | $this->deployer->setAnsibleUser($this->getConfig()->ANSIBLE_USER); 60 | } 61 | 62 | /** 63 | * @param $argc 64 | * @param array $argv 65 | * @return mixed 66 | * @throws Exception\CommandNotFoundException 67 | * @throws InvalidArgumentCountException 68 | */ 69 | public function runCommand($argc, array $argv) 70 | { 71 | $namespace = isset($argv[1]) ? $argv[1] : null; 72 | $command = isset($argv[2]) ? $argv[2] : null; 73 | $arguments = array_slice($argv, 3); 74 | 75 | if ($namespace == null) { 76 | $this->getPrinter()->printBanner(); 77 | $this->getPrinter()->printUsage(); 78 | exit; 79 | } 80 | 81 | if ($command == null) { 82 | $controller = $this->command_registry->getController($namespace); 83 | if ($controller === null) { 84 | $this->getPrinter()->newline(); 85 | $this->getPrinter()->out("Command not found.", "error_alt"); 86 | $this->getPrinter()->newline(); 87 | exit; 88 | } 89 | 90 | $controller->defaultCommand(); 91 | exit; 92 | } 93 | 94 | try { 95 | return $this->command_registry->runCommand($namespace, $command, $arguments); 96 | } catch (CommandNotFoundException $e) { 97 | $this->getPrinter()->newline(); 98 | $this->getPrinter()->out("Command not found.", "error_alt"); 99 | $this->getPrinter()->newline(); 100 | } 101 | 102 | return null; 103 | } 104 | 105 | /** 106 | * @return Config 107 | */ 108 | public function getConfig() 109 | { 110 | return $this->config; 111 | } 112 | 113 | /** 114 | * @param Config $config 115 | */ 116 | public function setConfig($config) 117 | { 118 | $this->config = $config; 119 | } 120 | 121 | /** 122 | * @return DigitalOcean 123 | */ 124 | public function getDO() 125 | { 126 | return $this->do; 127 | } 128 | 129 | /** 130 | * @return Deployer 131 | */ 132 | public function getDeployer() 133 | { 134 | return $this->deployer; 135 | } 136 | 137 | /** 138 | * @return CommandRegistry 139 | */ 140 | public function getCommandRegistry() 141 | { 142 | return $this->command_registry; 143 | } 144 | 145 | /** 146 | * @return CLIPrinter 147 | */ 148 | public function getPrinter() 149 | { 150 | return $this->printer; 151 | } 152 | } -------------------------------------------------------------------------------- /src/Command/FetchController.php: -------------------------------------------------------------------------------- 1 | flagExists('--force-update') ? 1 : 0; 27 | 28 | if ($this->flagExists('--force-cache')) { 29 | $force_update = -1; 30 | } 31 | 32 | $regions = $this->getDolphin()->getDO()->getRegions($force_update); 33 | 34 | if ($regions === null) { 35 | $this->getPrinter()->error("No Regions found."); 36 | exit; 37 | } 38 | 39 | $print_table[] = [ 'NAME', 'SLUG', 'AVAILABLE']; 40 | 41 | foreach ($regions as $region_info) { 42 | $region = new Region($region_info); 43 | $print_table[] = [ 44 | $region->name, 45 | $region->slug, 46 | $region->available, 47 | ]; 48 | } 49 | 50 | $this->getPrinter()->printTable($print_table); 51 | } 52 | 53 | /** 54 | * Gets available Droplet Sizes 55 | * @throws \Dolphin\Exception\APIException 56 | */ 57 | public function getSizes() 58 | { 59 | $force_update = $this->flagExists('--force-update') ? 1 : 0; 60 | 61 | if ($this->flagExists('--force-cache')) { 62 | $force_update = -1; 63 | } 64 | 65 | $sizes = $this->getDolphin()->getDO()->getSizes($force_update); 66 | 67 | if ($sizes === null) { 68 | $this->getPrinter()->error("No Sizes found."); 69 | exit; 70 | } 71 | 72 | $print_table[] = [ 'SLUG', 'MEMORY', 'VCPUS', 'DISK', 'TRANSFER', 'PRICE/MONTH']; 73 | 74 | foreach ($sizes as $size_info) { 75 | $size = new Size($size_info); 76 | $print_table[] = [ 77 | $size->slug, 78 | $size->memory . 'MB', 79 | $size->vcpus, 80 | $size->disk . 'GB', 81 | $size->transfer . 'TB', 82 | '$' .$size->price_monthly 83 | ]; 84 | } 85 | 86 | $this->getPrinter()->printTable($print_table); 87 | } 88 | 89 | /** 90 | * Gets available distro images 91 | * @throws \Dolphin\Exception\APIException 92 | */ 93 | public function getImages() 94 | { 95 | $force_update = $this->flagExists('--force-update') ? 1 : 0; 96 | 97 | if ($this->flagExists('--force-cache')) { 98 | $force_update = -1; 99 | } 100 | 101 | $images = $this->getDolphin()->getDO()->getImages($force_update); 102 | 103 | if ($images === null) { 104 | $this->getPrinter()->error("No Images found."); 105 | exit; 106 | } 107 | 108 | $print_table[] = [ 'ID', 'NAME', 'DIST', 'SLUG', 'TYPE', 'MIN_DISK_SIZE', 'VISIBILITY']; 109 | 110 | foreach ($images as $image_info) { 111 | $image = new Image($image_info); 112 | $print_table[] = [ 113 | $image->id, 114 | $image->name, 115 | $image->distribution, 116 | $image->slug, 117 | $image->type, 118 | $image->min_disk_size ? $image->min_disk_size . 'GB' : '-', 119 | $image->public ? 'public' : 'private', 120 | ]; 121 | } 122 | 123 | $this->getPrinter()->printTable($print_table); 124 | } 125 | 126 | public function getKeys() 127 | { 128 | $force_update = $this->flagExists('--force-update') ? 1 : 0; 129 | 130 | if ($this->flagExists('--force-cache')) { 131 | $force_update = -1; 132 | } 133 | 134 | $keys = $this->getDolphin()->getDO()->getKeys($force_update); 135 | 136 | if ($keys === null) { 137 | $this->getPrinter()->error("No SSH Keys found."); 138 | exit; 139 | } 140 | 141 | $print_table[] = [ 'ID', 'NAME', 'FINGERPRINT' ]; 142 | 143 | foreach ($keys as $key_info) { 144 | $key = new Key($key_info); 145 | $print_table[] = [ 146 | $key->id, 147 | $key->name, 148 | $key->fingerprint, 149 | ]; 150 | } 151 | 152 | $this->getPrinter()->printTable($print_table); 153 | } 154 | 155 | public function getCommandMap() 156 | { 157 | return [ 158 | 'images' => 'getImages', 159 | 'regions' => 'getRegions', 160 | 'sizes' => 'getSizes', 161 | 'keys' => 'getKeys', 162 | ]; 163 | } 164 | 165 | public function defaultCommand() 166 | { 167 | $this->output("Usage: ./dolphin ansible inventory", "info"); 168 | $this->getPrinter()->newline(); 169 | } 170 | 171 | } -------------------------------------------------------------------------------- /src/Command/DropletController.php: -------------------------------------------------------------------------------- 1 | flagExists('--force-update') ? 1 : 0; 26 | 27 | if ($this->flagExists('--force-cache')) { 28 | $force_update = -1; 29 | } 30 | 31 | $droplets = $this->getDolphin()->getDO()->getDroplets($force_update); 32 | 33 | if ($droplets === null) { 34 | $this->getPrinter()->error("No Droplets found."); 35 | exit; 36 | } 37 | 38 | $print_table[] = [ 'ID', 'NAME', 'IMAGE', 'IP', 'REGION', 'SIZE']; 39 | 40 | foreach ($droplets as $droplet_info) { 41 | $droplet = new Droplet($droplet_info); 42 | $print_table[] = [ 43 | $droplet->id, 44 | $droplet->name, 45 | $droplet->image['slug'], 46 | $droplet->networks['v4'][0]['ip_address'], 47 | $droplet->region['slug'], 48 | $droplet->size_slug, 49 | ]; 50 | } 51 | 52 | $this->getPrinter()->printTable($print_table); 53 | } 54 | 55 | /** 56 | * Gets detailed information about a droplet. 57 | * usage: ./dolphin droplet info DROPLET_ID [force-update] 58 | */ 59 | public function infoDroplet(array $arguments) 60 | { 61 | $force_update = $this->flagExists('--force-update') ? 1 : 0; 62 | 63 | if ($this->flagExists('--force-cache')) { 64 | $force_update = -1; 65 | } 66 | 67 | $droplet_id = $arguments[0]; 68 | if (!$droplet_id) { 69 | $this->getPrinter()->error("You must provide the droplet ID."); 70 | exit; 71 | } 72 | 73 | $this->getPrinter()->newline(); 74 | $this->getPrinter()->out(sprintf("Fetching Droplet info for ID %s...", $droplet_id), "alt"); 75 | 76 | try { 77 | $droplet = $this->getDolphin()->getDO()->getDroplet($droplet_id, $force_update); 78 | 79 | $this->getPrinter()->newline(); 80 | print_r($droplet); 81 | 82 | } catch (APIException $e) { 83 | $this->getPrinter()->error("An API error occurred."); 84 | $this->getPrinter()->out("Response Info:"); 85 | $this->getPrinter()->newline(); 86 | 87 | print_r($this->getDolphin()->getDO()->getLastResponse()); 88 | exit; 89 | } 90 | } 91 | 92 | /** 93 | * Creates a new droplet using default options from config.php 94 | * usage: ./dolphin droplet create name=MY_DROPLET_NAME [api_param2=api_value2 api_param3=api_value3] 95 | * 96 | * @param array $arguments 97 | * @throws \Dolphin\Exception\MissingArgumentException 98 | */ 99 | public function createDroplet(array $arguments) 100 | { 101 | $params = $this->parseArgs($arguments); 102 | 103 | if (!isset($params['name'])) { 104 | $params['name'] = RandomNameProvider::getName(); 105 | } 106 | 107 | $this->getPrinter()->info("Creating new Droplet..."); 108 | 109 | try { 110 | $response = $this->getDolphin()->getDO()->createDroplet($params); 111 | $this->getPrinter()->success( 112 | sprintf("Your new droplet \"%s\" was successfully created. Please notice it might take a few minutes for the network to be ready.\nHere's some info:", $params['name']) 113 | ); 114 | 115 | $response_body = json_decode($response['body'], true); 116 | $droplet = new Droplet($response_body['droplet']); 117 | 118 | $table[] = [ 'id', 'name', 'region', 'size', 'image', 'created at' ]; 119 | $table[] = [ 120 | $droplet->id, 121 | $droplet->name, 122 | $droplet->region['slug'], 123 | $droplet->size_slug, 124 | $droplet->image['slug'], 125 | $droplet->created_at, 126 | ]; 127 | 128 | $this->getPrinter()->printTable($table); 129 | 130 | } catch (APIException $e) { 131 | $this->getPrinter()->error("An API error has ocurred."); 132 | $this->getPrinter()->out("Response Info:"); 133 | $this->getPrinter()->newline(); 134 | 135 | print_r($this->getDolphin()->getDO()->getLastResponse()); 136 | } 137 | 138 | $this->getPrinter()->newline(); 139 | } 140 | 141 | /** 142 | * Deletes a Droplet. 143 | * usage: ./dolphin destroy DROPLET_ID 144 | * 145 | * @param array $arguments 146 | * @throws APIException 147 | */ 148 | public function destroyDroplet(array $arguments) 149 | { 150 | $droplet_id = $arguments[0]; 151 | if (!$droplet_id) { 152 | $this->getPrinter()->error("Error: You must provide the droplet ID."); 153 | exit; 154 | } 155 | 156 | $this->getPrinter()->info(sprintf("Destroying Droplet ID %s ...", $droplet_id)); 157 | 158 | if ($this->getDolphin()->getDO()->destroyDroplet($droplet_id)) { 159 | $this->getPrinter()->success("Droplet successfully destroyed.\n\n"); 160 | } 161 | } 162 | 163 | /** 164 | * @return array 165 | */ 166 | public function getCommandMap() 167 | { 168 | return [ 169 | 'list' => 'listDroplets', 170 | 'create' => 'createDroplet', 171 | 'destroy' => 'destroyDroplet', 172 | 'info' => 'infoDroplet', 173 | ]; 174 | } 175 | 176 | /** 177 | * Default command to be executed when no extra arguments are passed. 178 | */ 179 | public function defaultCommand() 180 | { 181 | $this->output("Usage: ./dolphin droplet [list|info|create|destroy]", "info"); 182 | $this->getPrinter()->newline(); 183 | } 184 | } -------------------------------------------------------------------------------- /src/Core/CLIPrinter.php: -------------------------------------------------------------------------------- 1 | themes['regular'] = [ 33 | 'default' => [ self::$FG_WHITE ], 34 | 'alt' => [ self::$FG_BLACK, self::$BG_WHITE ], 35 | 'error' => [ self::$FG_RED ], 36 | 'error_alt' => [ self::$FG_WHITE, self::$BG_RED ], 37 | 'success' => [ self::$FG_GREEN ], 38 | 'success_alt' => [ self::$FG_WHITE, self::$BG_GREEN ], 39 | 'info' => [ self::$FG_CYAN], 40 | 'info_alt' => [ self::$FG_WHITE, self::$BG_CYAN ] 41 | ]; 42 | 43 | $this->themes['unicorn'] = [ 44 | 'default' => [ self::$FG_CYAN ], 45 | 'alt' => [ self::$FG_BLACK, self::$BG_CYAN ], 46 | 'error' => [ self::$FG_RED ], 47 | 'error_alt' => [ self::$FG_CYAN, self::$BG_RED ], 48 | 'success' => [ self::$FG_GREEN ], 49 | 'success_alt' => [ self::$FG_BLACK, self::$BG_GREEN ], 50 | 'info' => [ self::$FG_MAGENTA], 51 | 'info_alt' => [ self::$FG_WHITE, self::$BG_MAGENTA ] 52 | ]; 53 | 54 | $this->palette = isset($this->themes[$palette]) ? $this->themes[$palette] : $this->themes['regular']; 55 | } 56 | 57 | public function format($message, $style = "default") 58 | { 59 | $style_colors = $this->getPalette($style); 60 | 61 | $bg = ''; 62 | if (isset($style_colors[1])) { 63 | $bg = ';' . $style_colors[1]; 64 | } 65 | 66 | $output = sprintf("\e[%s%sm%s\e[0m", $style_colors[0], $bg, $message); 67 | 68 | return $output; 69 | } 70 | 71 | public function getPalette($style) 72 | { 73 | return isset($this->palette[$style]) ? $this->palette[$style] : "default"; 74 | } 75 | 76 | public function out($message, $style = "default") 77 | { 78 | echo $this->format($message, $style); 79 | } 80 | 81 | public function error($message) 82 | { 83 | $this->newline(); 84 | $this->out($message, "error"); 85 | $this->newline(); 86 | } 87 | 88 | public function info($message) 89 | { 90 | $this->newline(); 91 | $this->out($message, "info"); 92 | $this->newline(); 93 | } 94 | 95 | public function success($message) 96 | { 97 | $this->newline(); 98 | $this->out($message, "success"); 99 | $this->newline(); 100 | } 101 | 102 | public function newline() 103 | { 104 | $this->out("\n"); 105 | } 106 | 107 | /** 108 | * Prints Dolphin Banner 109 | */ 110 | public function printBanner() 111 | { 112 | $header = ' 113 | ,gggggggggggg, 114 | dP"""88""""""Y8b, ,dPYb, ,dPYb, 115 | Yb, 88 `8b, IP\'`Yb IP\'`Yb 116 | `" 88 `8b I8 8I I8 8I gg 117 | 88 Y8 I8 8\' I8 8\' "" 118 | 88 d8 ,ggggg, I8 dP gg,gggg, I8 dPgg, gg ,ggg,,ggg, 119 | 88 ,8P dP" "Y8ggg I8dP I8P" "Yb I8dP" "8I 88 ,8" "8P" "8, 120 | 88 ,8P\' i8\' ,8I I8P I8\' ,8i I8P I8 88 I8 8I 8I 121 | 88______,dP\' ,d8, ,d8\' ,d8b,_ ,I8 _ ,d8\' ,d8 I8,_,88,_,dP 8I Yb, 122 | 888888888P" P"Y8888P" 8P\'"Y88PI8 YY88888P88P `Y88P""Y88P\' 8I `Y8 123 | I8 124 | I8 125 | I8 126 | I8 127 | I8 128 | I8 129 | '; 130 | 131 | $this->out($header, "info"); 132 | $this->newline(); 133 | } 134 | 135 | /** 136 | * Prints Doplhin basic usage 137 | */ 138 | public function printUsage() 139 | { 140 | $this->out("Usage: ./dolphin [command] [sub-command] [params]", "info_alt"); 141 | $this->newline(); 142 | $this->out("For help, use ./dolphin help", "info"); 143 | $this->newline(); 144 | } 145 | 146 | /** 147 | * @param array $table 148 | * @param int $min_col_size 149 | * @param bool $with_header 150 | */ 151 | public function printTable(array $table, $min_col_size = 10, $with_header = true, $spacing = true) 152 | { 153 | $first = true; 154 | 155 | if ($spacing) { 156 | $this->newline(); 157 | } 158 | 159 | foreach ($table as $index => $row) { 160 | 161 | $style = "default"; 162 | if ($first && $with_header) { 163 | $style = "info_alt"; 164 | } 165 | 166 | $this->printRow($table, $index, $style, $min_col_size); 167 | $first = false; 168 | } 169 | 170 | if ($spacing) { 171 | $this->newline(); 172 | } 173 | } 174 | 175 | /** 176 | * @param array $table 177 | * @param int $row 178 | * @param string $style 179 | * @param int $min_col_size 180 | */ 181 | public function printRow(array $table, $row, $style = "default", $min_col_size = 5) 182 | { 183 | 184 | foreach ($table[$row] as $column => $table_cell) { 185 | $col_size = $this->calculateColumnSize($column, $table, $min_col_size); 186 | 187 | $this->printCell($table_cell, $style, $col_size); 188 | } 189 | 190 | $this->out("\n"); 191 | } 192 | 193 | /** 194 | * @param string $table_cell 195 | * @param string $style 196 | * @param int $col_size 197 | */ 198 | protected function printCell($table_cell, $style = "default", $col_size = 5) 199 | { 200 | $table_cell = str_pad($table_cell, $col_size); 201 | $this->out($table_cell, $style); 202 | } 203 | 204 | /** 205 | * @param $column 206 | * @param array $table 207 | * @param int $min_col_size 208 | * @return int 209 | */ 210 | protected function calculateColumnSize($column, array $table, $min_col_size = 5) 211 | { 212 | $size = $min_col_size; 213 | 214 | foreach ($table as $row) { 215 | $size = strlen($row[$column]) > $size ? strlen($row[$column]) + 2 : $size; 216 | } 217 | 218 | return $size; 219 | } 220 | } -------------------------------------------------------------------------------- /src/DigitalOcean.php: -------------------------------------------------------------------------------- 1 | api_token = $api_token; 48 | $this->cache = $cache; 49 | $this->agent = $agent; 50 | $this->config = $config; 51 | } 52 | 53 | /** 54 | * @return string 55 | */ 56 | public function getLastResponse() 57 | { 58 | return $this->last_response; 59 | } 60 | 61 | /** 62 | * @param int $force_update 63 | * @return null 64 | * @throws APIException 65 | */ 66 | public function getKeys($force_update = 0) 67 | { 68 | $response = $this->get(self::$API_KEYS, [], $force_update); 69 | 70 | if ($response['code'] != 200) { 71 | throw new APIException("Invalid response code."); 72 | } 73 | 74 | $response_body = json_decode($response['body'], true); 75 | 76 | return isset($response_body['ssh_keys']) ? $response_body['ssh_keys'] : null; 77 | } 78 | 79 | /** 80 | * @param int $force_update 81 | * @param string $type 82 | * @return null 83 | * @throws APIException 84 | */ 85 | public function getImages($force_update = 0, $type = "distribution") 86 | { 87 | $response = $this->get(self::$API_IMAGES . "?type=$type", [], $force_update); 88 | 89 | if ($response['code'] != 200) { 90 | throw new APIException("Invalid response code."); 91 | } 92 | 93 | $response_body = json_decode($response['body'], true); 94 | 95 | return isset($response_body['images']) ? $response_body['images'] : null; 96 | } 97 | 98 | /** 99 | * @param int $force_update 100 | * @return null 101 | * @throws APIException 102 | */ 103 | public function getRegions($force_update = 0) 104 | { 105 | $response = $this->get(self::$API_REGIONS, [], $force_update); 106 | 107 | if ($response['code'] != 200) { 108 | throw new APIException("Invalid response code."); 109 | } 110 | 111 | $response_body = json_decode($response['body'], true); 112 | 113 | return isset($response_body['regions']) ? $response_body['regions'] : null; 114 | } 115 | 116 | /** 117 | * @param int $force_update 118 | * @return null 119 | * @throws APIException 120 | */ 121 | public function getSizes($force_update = 0) 122 | { 123 | $response = $this->get(self::$API_SIZES, [], $force_update); 124 | 125 | if ($response['code'] != 200) { 126 | throw new APIException("Invalid response code."); 127 | } 128 | 129 | $response_body = json_decode($response['body'], true); 130 | 131 | return isset($response_body['sizes']) ? $response_body['sizes'] : null; 132 | } 133 | 134 | /** 135 | * Gets all droplets 136 | * @return array | null 137 | * @param int $force_update Whether force a cache update or not 138 | * @throws APIException 139 | */ 140 | public function getDroplets($force_update = 0) 141 | { 142 | $response = $this->get(self::$API_DROPLET, [], $force_update); 143 | 144 | if ($response['code'] != 200) { 145 | throw new APIException("Invalid response code."); 146 | } 147 | 148 | $response_body = json_decode($response['body'], true); 149 | 150 | return isset($response_body['droplets']) ? $response_body['droplets'] : null; 151 | } 152 | 153 | /** 154 | * Gets information about a single droplet 155 | * @param $droplet_id 156 | * @param int $force_update 157 | * @return null 158 | * @throws APIException 159 | */ 160 | public function getDroplet($droplet_id, $force_update = 0) 161 | { 162 | $response = $this->get(self::$API_DROPLET . '/' . $droplet_id, [], $force_update); 163 | 164 | if (!in_array($response['code'], [200, 202, 204])) { 165 | throw new APIException("Invalid response code."); 166 | } 167 | 168 | $response_body = json_decode($response['body'], true); 169 | 170 | return isset($response_body['droplet']) ? $response_body['droplet'] : null; 171 | } 172 | 173 | /** 174 | * Creates a new droplet. 175 | * @param array $params Droplet parameters. The only mandatory item is 'name'. 176 | * @return mixed 177 | * @throws MissingArgumentException 178 | * @throws APIException 179 | */ 180 | public function createDroplet(array $params) 181 | { 182 | if (!isset($params['name'])) { 183 | throw new MissingArgumentException("Missing the 'name' parameter."); 184 | } 185 | 186 | $params = array_merge([ 187 | 'region' => $this->config['D_REGION'], 188 | 'size' => $this->config['D_SIZE'], 189 | 'image' => $this->config['D_IMAGE'], 190 | 'tags' => $this->config['D_TAGS'], 191 | 'ssh_keys' => $this->config['D_SSH_KEYS'], 192 | ], $params); 193 | 194 | $response = $this->post(self::$API_DROPLET, $params); 195 | 196 | if (!in_array($response['code'], [200, 202, 204])) { 197 | throw new APIException("An API error occurred."); 198 | } 199 | 200 | return $response; 201 | } 202 | 203 | /** 204 | * Destroys a Droplet 205 | * @param $droplet_id 206 | * @return bool 207 | * @throws APIException 208 | */ 209 | public function destroyDroplet($droplet_id) 210 | { 211 | $response = $this->delete(self::$API_DROPLET . '/' . $droplet_id); 212 | 213 | if (!in_array($response['code'], [200, 202, 204])) { 214 | throw new APIException("An API error occurred."); 215 | } 216 | 217 | return true; 218 | } 219 | 220 | /** 221 | * Makes a GET query 222 | * @param string $endpoint API endpoint 223 | * @param array $custom_headers optional custom headers 224 | * @param int $force_update 1 to force update, -1 to force cached (default is 0) 225 | * @return mixed 226 | */ 227 | public function get($endpoint, array $custom_headers = [], $force_update = 0) 228 | { 229 | if ($force_update < 1) { 230 | 231 | if ($force_update == -1) { 232 | $cached = $this->cache->getCached($endpoint); 233 | } else { 234 | $cached = $this->cache->getCachedUnlessExpired($endpoint); 235 | } 236 | 237 | if ($cached !== null) { 238 | return [ 'code' => 200, 'body' => $cached ]; 239 | } 240 | } 241 | 242 | $headers = array_merge($this->getDefaultHeaders(), $custom_headers); 243 | 244 | $this->last_response = $this->agent->get($endpoint, $headers); 245 | 246 | $this->cache->save($this->last_response['body'], $endpoint); 247 | 248 | return $this->last_response; 249 | } 250 | 251 | /** 252 | * Makes a POST query 253 | * @param $endpoint 254 | * @param array $params 255 | * @param array $custom_headers 256 | * @return array 257 | */ 258 | public function post($endpoint, array $params, $custom_headers = []) 259 | { 260 | $headers = array_merge($this->getDefaultHeaders(), $custom_headers); 261 | 262 | $this->last_response = $this->agent->post($endpoint, $params, $headers); 263 | 264 | return $this->last_response; 265 | } 266 | 267 | /** 268 | * Makes a DELETE query 269 | * @param $endpoint 270 | * @param array $custom_headers 271 | * @return array 272 | */ 273 | public function delete($endpoint, $custom_headers = []) 274 | { 275 | $headers = array_merge($this->getDefaultHeaders(), $custom_headers); 276 | 277 | $this->last_response = $this->agent->delete($endpoint, $headers); 278 | 279 | return $this->last_response; 280 | } 281 | 282 | /** 283 | * @return array 284 | */ 285 | protected function getDefaultHeaders() 286 | { 287 | $headers[] = "Content-type: application/json"; 288 | $headers[] = "Authorization: Bearer $this->api_token"; 289 | 290 | return $headers; 291 | } 292 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ,gggggggggggg, 3 | dP"""88""""""Y8b, ,dPYb, ,dPYb, 4 | Yb, 88 `8b, IP'`Yb IP'`Yb 5 | `" 88 `8b I8 8I I8 8I gg 6 | 88 Y8 I8 8' I8 8' "" 7 | 88 d8 ,ggggg, I8 dP gg,gggg, I8 dPgg, gg ,ggg,,ggg, 8 | 88 ,8P dP" "Y8ggg I8dP I8P" "Yb I8dP" "8I 88 ,8" "8P" "8, 9 | 88 ,8P' i8' ,8I I8P I8' ,8i I8P I8 88 I8 8I 8I 10 | 88______,dP' ,d8, ,d8' ,d8b,_ ,I8 _ ,d8' ,d8 I8,_,88,_,dP 8I Yb, 11 | 888888888P" P"Y8888P" 8P'"Y88PI8 YY88888P88P `Y88P""Y88P' 8I `Y8 12 | I8 13 | I8 14 | I8 15 | I8 16 | I8 17 | I8 18 | ``` 19 | 20 | # Dolphin 21 | 22 | Dolphin is a command line tool created for quickly bootstrapping environments based on pre-defined models, using Ansible to automate server setup. 23 | 24 | It is built for **CLI ONLY**, in "vanilla" PHP, with no user-land library dependencies. Composer is required for setting up autoload, and Curl is required for the API work. 25 | 26 | Dolphin uses [DigitalOcean](https://digitalocean.com) as cloud provider. 27 | 28 | _Note: This is a WORK IN PROGRESS._ 29 | 30 | ## Requirements 31 | 32 | - PHP (cli) 33 | - Composer 34 | - Curl 35 | - Valid DigitalOcean API Key (R+W) 36 | 37 | ## Installation 38 | 39 | ### Via Git 40 | First, clone this repository with: 41 | 42 | ``` 43 | git clone 44 | ``` 45 | 46 | Now go to Dolphin's directory and set the permissions for the executable: 47 | 48 | ``` 49 | cd dolphin 50 | chmod +x dolphin.php 51 | ``` 52 | 53 | Run `composer install` to set up autoload: 54 | 55 | ``` 56 | composer install 57 | ``` 58 | 59 | ## Usage 60 | 61 | A `config.php` file is created upon installation with Composer. Edit the contents of this file and adjust the values accordingly: 62 | 63 | ``` 64 | return $dolphin_config = [ 65 | 66 | //CLI THEME 67 | 'THEME' => 'default', 68 | 69 | // DigitalOcean API Token 70 | 'DO_API_TOKEN' => 'YOUR_DIGITALOCEAN_API_TOKEN', 71 | 72 | //Default Ansible server group 73 | 'DEFAULT_SERVER_GROUP' => 'servers', 74 | 75 | //Cache location relative to doc root */ 76 | 'CACHE_DIR' => 'var/cache', 77 | 78 | //Cache expiry time in minutes 79 | 'CACHE_EXPIRY' => 60, 80 | 81 | //Default Droplet Settings 82 | 'DO' => [ 83 | 'D_REGION' => 'nyc3', 84 | 'D_IMAGE' => 'ubuntu-18-04-x64', 85 | 'D_SIZE' => 's-1vcpu-1gb', 86 | 'D_TAGS' => [ 87 | 'dolphin' 88 | ], 89 | 90 | // Optional - SSH key(s) to be included for the root user in new droplets. 91 | // Uncomment and add your own key(s) - ID or Fingerprint 92 | // You can list your registered keys with: ./dolphin fetch keys 93 | //'D_SSH_KEYS' => [ 94 | // 'YOUR_SSH_KEY_ID_OR_FINGERPRINT' 95 | //], 96 | ], 97 | 98 | //Default Ansible Settings 99 | 'ANSIBLE_USER' => 'sammy', 100 | 'PLAYBOOKS_DIR' => 'playbooks', 101 | 102 | ]; 103 | ``` 104 | 105 | Now you can execute Dolphin with: 106 | 107 | ``` 108 | ./dolphin.php [command] [sub-command] [params] 109 | ``` 110 | 111 | For an overall look of commands and sub-commands, run `./dolphin.php help`. 112 | 113 | ### Installing Globally: (optional) 114 | 115 | If you'd like to use dolphin out of any directory in a global installation, you can do so by creating a symbolic link to the dolphin executable on `/usr/local/bin`. Please notice this will only work for your current user, who owns the `dolphin` directory. 116 | 117 | ``` 118 | sudo ln -s /usr/local/bin/dolphin /home/erika/Projects/dolphin/dolphin.php 119 | ``` 120 | 121 | ## Droplet Commands: `dolphin droplet` 122 | 123 | The following commands can be used to manage droplets. 124 | 125 | ### Listing Droplets 126 | 127 | ```command 128 | dolphin droplet list 129 | ``` 130 | 131 | This will show a list with your DigitalOcean droplets (ID, name, IP, region and size). 132 | 133 | ``` 134 | ID NAME IP REGION SIZE 135 | 140295122 ubuntu-1804-01 188.166.115.68 ams3 s-1vcpu-2gb 136 | 140295123 ubuntu-1804-02 188.166.123.245 ams3 s-1vcpu-2gb 137 | 140295124 ubuntu-1804-03 174.138.13.97 ams3 s-1vcpu-2gb 138 | 142352633 mysql-wordpress 165.22.254.246 sgp1 s-2vcpu-4gb 139 | 142807570 ubuntu-s-1vcpu-1gb-ams3-01 167.99.217.247 ams3 s-1vcpu-1gb 140 | ``` 141 | 142 | 143 | ### Getting Information About a Droplet 144 | 145 | ``` 146 | dolphin droplet info DROPLET_ID 147 | ``` 148 | 149 | The output will be a JSON will all the available information about that droplet. 150 | 151 | ### Creating a New Droplet 152 | Uses default options from your config file, but you can override any of the API query parameters. 153 | Parameters should be passed as `name=value` items. If you don't provide a name, it will be automatically generated for you. 154 | 155 | Creating a new droplet with default options and random name: 156 | 157 | ``` 158 | dolphin droplet create 159 | ``` 160 | 161 | You will see output like this: 162 | 163 | ``` 164 | Creating new Droplet... 165 | 166 | Your new droplet "fine-shark" was successfully created. Please notice it might take a few minutes for the network to be ready. 167 | Here's some info: 168 | 169 | id name region size image created at 170 | 155243337 fine-shark fra1 s-2vcpu-4gb ubuntu-18-04-x64 2019-08-17T06:20:35Z 171 | 172 | ``` 173 | 174 | 175 | It will take a few moments before the network is ready and you're able to SSH or run `droplet deployer` on that server. To get the IP address, run this command after a few seconds: 176 | 177 | ``` 178 | dolphin droplet list --force-update 179 | ``` 180 | 181 | This will show an updated list of your Droplets, including the newly created one. 182 | 183 | Now let's say you want to use a custom name, region and droplet size: 184 | 185 | ``` 186 | dolphin droplet create name=MyDropletName size=s-2vcpu-4gb region=fra1 187 | ``` 188 | 189 | Check the [DigitalOCean API documentation](https://developers.digitalocean.com/documentation/v2/#create-a-new-droplet) for more information on all the parameters you can use when creating new Droplets. 190 | 191 | ### Destroying a Droplet 192 | You can obtain the ID of a Droplet by running `droplet list` to list all your droplets. 193 | 194 | ``` 195 | dolphin droplet destroy DROPLET_ID 196 | ``` 197 | 198 | ## Checking for Information: `dolphin fetch` 199 | 200 | To get a list of all available regions you can use when creating a new Droplet, use: 201 | 202 | ``` 203 | dolphin fetch regions 204 | ``` 205 | 206 | ``` 207 | NAME SLUG AVAILABLE 208 | New York 1 nyc1 1 209 | San Francisco 1 sfo1 1 210 | New York 2 nyc2 1 211 | Amsterdam 2 ams2 1 212 | Singapore 1 sgp1 1 213 | London 1 lon1 1 214 | New York 3 nyc3 1 215 | Amsterdam 3 ams3 1 216 | Frankfurt 1 fra1 1 217 | Toronto 1 tor1 1 218 | San Francisco 2 sfo2 1 219 | Bangalore 1 blr1 1 220 | ``` 221 | 222 | 223 | To get a list of all available sizes you can use when creating a new Droplet, use: 224 | 225 | ``` 226 | dolphin fetch sizes 227 | ``` 228 | 229 | ``` 230 | SLUG MEMORY VCPUS DISK TRANSFER PRICE/MONTH 231 | 512mb 512MB 1 20GB 1TB $5 232 | s-1vcpu-1gb 1024MB 1 25GB 1TB $5 233 | 1gb 1024MB 1 30GB 2TB $10 234 | s-1vcpu-2gb 2048MB 1 50GB 2TB $10 235 | s-1vcpu-3gb 3072MB 1 60GB 3TB $15 236 | s-2vcpu-2gb 2048MB 2 60GB 3TB $15 237 | s-3vcpu-1gb 1024MB 3 60GB 3TB $15 238 | 2gb 2048MB 2 40GB 3TB $20 239 | s-2vcpu-4gb 4096MB 2 80GB 4TB $20 240 | 4gb 4096MB 2 60GB 4TB $40 241 | c-2 4096MB 2 25GB 4TB $40 242 | m-1vcpu-8gb 8192MB 1 40GB 5TB $40 243 | s-4vcpu-8gb 8192MB 4 160GB 5TB $40 244 | g-2vcpu-8gb 8192MB 2 25GB 4TB $60 245 | gd-2vcpu-8gb 8192MB 2 50GB 4TB $65 246 | m-16gb 16384MB 2 60GB 5TB $75 247 | 8gb 8192MB 4 80GB 5TB $80 248 | c-4 8192MB 4 50GB 5TB $80 249 | s-6vcpu-16gb 16384MB 6 320GB 6TB $80 250 | g-4vcpu-16gb 16384MB 4 50GB 5TB $120 251 | ``` 252 | 253 | To get a list of all registered SSH Keys you can use when creating a new Droplet, use: 254 | 255 | ``` 256 | dolphin fetch keys 257 | ``` 258 | 259 | ``` 260 | ID NAME FINGERPRINT 261 | 23936699 heidislab e7:51:a3:7e:e1:11:1b:d1:69:8e:98:3d:45:5f:7f:14 262 | ``` 263 | 264 | ## Deployer Commands: `dolphin deployer` 265 | 266 | The `deployer` module can be used to run set up remote servers using Ansible playbooks. 267 | A basic library of playbooks is included in the `playbooks` folder, but users are encouraged to create their own playbook library. 268 | 269 | ### Getting Ansible version 270 | 271 | ``` 272 | dolphin deployer info 273 | ``` 274 | 275 | ``` 276 | Ansible Version: 277 | ansible 2.8.3 278 | config file = /etc/ansible/ansible.cfg 279 | configured module search path = [u'/home/erika/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules'] 280 | ansible python module location = /usr/lib/python2.7/dist-packages/ansible 281 | executable location = /usr/bin/ansible 282 | python version = 2.7.15+ (default, Nov 27 2018, 23:36:35) [GCC 7.3.0] 283 | 284 | Playbooks Dir: 285 | /home/erika/Projects/dolphin/src/../playbooks 286 | 287 | ``` 288 | 289 | ### Ping a Host 290 | 291 | ``` 292 | dolphin deployer ping fine-shark 293 | ``` 294 | 295 | If you need to provide a different remote user than the one specified in your configuration file, you can do so by providing the `user` parameter: 296 | 297 | ``` 298 | dolphin deployer ping fine-shark user=root 299 | ``` 300 | 301 | ### Listing Available Scripts (Playbooks) 302 | 303 | ``` 304 | dolphin deployer list 305 | ``` 306 | 307 | ``` 308 | Deploy Scripts Currently Available 309 | 310 | default for ubuntu1804 311 | lemp for ubuntu1804 312 | 313 | You can run a script with: ./dolphin deployer run [script] on [droplet-name] 314 | 315 | ``` 316 | 317 | ### Running a Script on a Droplet 318 | 319 | ``` 320 | dolphin deployer run lemp on fine-shark 321 | ``` 322 | 323 | This will initiate the playbook execution on that host. Optionally, you can provide a `user` parameter to set the remote user when running the Ansible commands on the host: 324 | 325 | ``` 326 | dolphin deployer run default on fine-shark user=root 327 | ``` 328 | 329 | ## Tips & Tricks 330 | 331 | ### Manipulating Cache 332 | 333 | To optimize API querying and avoid hitting resource limits, Dolphin uses a simple file caching mechanism. 334 | 335 | To force a cache update, include the flag `--force-update`: 336 | 337 | ``` 338 | dolphin droplet list --force-update 339 | ``` 340 | 341 | If instead you'd like to enforce cache usage and not query for new results even if the cache timeout has been reached, you can use: 342 | 343 | ``` 344 | dolphin droplet list --force-cache 345 | ``` 346 | 347 | ### Using the Dynamic Inventory Script with Ansible 348 | 349 | If you'd like to use the dynamic inventory feature of Dolphin while running native Ansible commands and playbooks, you can use the included `hosts.php` dynamic inventory script, and you can also generate a static inventory file using the `dolphin inventory` command. 350 | 351 | #### Using the included Dynamic Inventory Script 352 | 353 | The included `hosts.php` script works as a dynamic inventory script that can be used directly with Ansible commands. 354 | 355 | ``` 356 | ansible all -m ping -i hosts.php 357 | ``` 358 | 359 | 360 | #### Building a static Inventory File 361 | 362 | You can generate dynamic inventories in INI or JSON format. The inventory is dynamically built based on your current active droplets. 363 | 364 | To generate a JSON inventory, run: 365 | 366 | `./dolphin ansible inventory json` 367 | 368 | Output: 369 | 370 | ``` 371 | { 372 | "servers": { 373 | "hosts": [ 374 | "docker02", 375 | "docker03", 376 | "docker04", 377 | "test1" 378 | ] 379 | }, 380 | "all": { 381 | "children": [ 382 | "ungrouped", 383 | "servers" 384 | ] 385 | }, 386 | "_meta": { 387 | "hostvars": { 388 | "docker02": { 389 | "ansible_host": "134.209.82.17", 390 | "ansible_python_interpreter": "/usr/bin/python3" 391 | }, 392 | "docker03": { 393 | "ansible_host": "134.209.205.231", 394 | "ansible_python_interpreter": "/usr/bin/python3" 395 | }, 396 | "docker04": { 397 | "ansible_host": "188.166.46.60", 398 | "ansible_python_interpreter": "/usr/bin/python3" 399 | }, 400 | "test1": { 401 | "ansible_host": "188.166.16.148", 402 | "ansible_python_interpreter": "/usr/bin/python3" 403 | } 404 | } 405 | } 406 | } 407 | 408 | ``` 409 | 410 | 411 | To generate an INI inventory, run: 412 | 413 | `./dolphin ansible inventory ini` 414 | 415 | Output: 416 | 417 | ``` 418 | [servers] 419 | docker02 ansible_host=134.209.82.17 420 | docker03 ansible_host=134.209.205.231 421 | docker04 ansible_host=188.166.46.60 422 | test1 ansible_host=188.166.16.148 423 | 424 | [all:vars] 425 | ansible_python_interpreter=/usr/bin/python3 426 | ``` 427 | 428 | If you want to save this as a static inventory file: 429 | 430 | ``` 431 | ./dolphin ansible inventory > inventory 432 | ``` 433 | 434 | 435 | Then you can run Ansible with: 436 | 437 | ``` 438 | ansible-playbook -i inventory myplaybook.yml 439 | ``` 440 | --------------------------------------------------------------------------------