#!/usr/bin/env ruby require "thor" require "netrc" require "highline/import" require "faraday" require "json" require "rainbow" class Acquia < Thor # A no_commands block is designed to show the methods that cannot be invoked # and as such, do not have a description. no_commands { # Internal: Used for outputting a pretty success message. # # Returns the coloured and formatted string. def success(text) puts "#{text}".foreground(:green) end # Internal: Used for outputting a pretty error message. # # Returns the coloured and formatted string. def fail(text) puts "#{text}".foreground(:red) end # Internal: Used for outputting a pretty info message. # # Returns the coloured and formatted string. def info(text) puts "#{text}".foreground(:cyan) end # Internal: Create a request to the Acquia API. # # The request generated contains all the correct user authentication and # headers. # # Returns a JSON string of the body. def acquia_api_call(resource, method = "GET", data = {}) n = Netrc.read @acquia_user, @acquia_password = n["cloudapi.acquia.com"] # Check if the user is behind a proxy and add the proxy settings if found. if using_proxy? conn = Faraday.new(:proxy => ENV["HTTPS_PROXY"]) else conn = Faraday.new end conn.basic_auth(@acquia_user, @acquia_password) case method when "GET" response = conn.get "https://cloudapi.acquia.com/v1/#{resource}.json" JSON.parse response.body when "POST" response = conn.post "https://cloudapi.acquia.com/v1/#{resource}.json", data.to_json JSON.parse response.body when "CODE-DEPLOY-POST" response = conn.post "https://cloudapi.acquia.com/v1/#{resource}.json?path=#{data[:release]}" JSON.parse response.body when "DELETE" response = conn.delete "https://cloudapi.acquia.com/v1/#{resource}.json" JSON.parse response.body else end end # Internal: Get defined subscription environments. # # This is a helper method that fetches all the available environments for a # subscription and returns them for use in other methods. # # Returns an array of environments. def get_acquia_environments(subscription) env_data = acquia_api_call "sites/#{subscription}/envs" envs = [] env_data.each do |env| envs << env["name"] end envs end # Internal: Truncate a SSH key to a secure and recognisable size. # # Displaying whole SSH keys is probably a bad idea so instead we are getting # the first 30 characters and the last 100 characters of the key and # separating them with an ellipis. This allows you to recognise the # important parts of the key instead of the whole thing. # # Returns string. def truncate_ssh_key(ssh_key) front_part = ssh_key[0...30] back_part = ssh_key[-50, 50] new_ssh_key = "#{front_part}...#{back_part}" end # Internal: Send a request to purge a domain's cache. # # Purge the web cache via an API call. # # Returns a status message. def purge_acquia_domain(subscription, environment, domain) # Ensure all the required fields are available. if subscription.nil? || environment.nil? || domain.nil? fail "Purge request is missing a required parameter." return end purge_request = acquia_api_call "sites/#{subscription}/envs/#{environment}/domains/#{domain}/cache", "DELETE" success "#{domain} has been successfully purged." if purge_request["id"] end # Internal: Check whether a proxy is in use. # # Return boolean based on whether HTTPS_PROXY is set. def using_proxy? if ENV["HTTPS_PROXY"] true else false end end # Internal: Output information on a database instance. def output_database_instance(database) say "> Username: #{database["username"]}" say "> Password: #{database["password"]}" say "> Host: #{database["host"]}" say "> DB cluster: #{database["db_cluster"]}" say "> Instance name: #{database["instance_name"]}" end # Internal: Output information for a single task item. def output_task_item(task) completion_time = (task["completed"].to_i - task["started"].to_i) / 60 say say "Task ID: #{task["id"].to_i}" say "Description: #{task["description"]}" say "Status: #{task["state"]}" # If the completion time is greater then 0, output it in minutes otherwise # just say it was less then a minute. if completion_time > 0 say "Compeletion time: About #{completion_time} minutes" else say "Compeletion time: Less than 1 minute" end say "Queue: #{task["queue"]}" end } # Public: Log into the Acquia Cloud API. # # This sets up the user account within the netrc file so that subsequent # calls can reuse the authentication without the user being prompted for it. # # Returns the status of your login attempt. desc "login", "Login to your Acquia account." def login user = ask "Enter your username:" password = ask "Enter your password:" # Update (or create if needed) the netrc file that will contain the user # authentication details. n = Netrc.read n.new_item_prefix = "# This entry was added for connecting to the Acquia Cloud API\n" n["cloudapi.acquia.com"] = user, password n.save success "Your user credentials have been successfully set." end # Public: Display an overview of the subscriptions. # # Returns all subscriptions with their respective data. desc "list-subscriptions", "Find all subscriptions that you have access to." def list_subscriptions subscriptions = acquia_api_call "sites" subscriptions.each do |subscription| say # Get the individual subscription information. subscription_info = acquia_api_call "sites/#{subscription}" say "#{subscription_info["title"]}" say "> Username: #{subscription_info["unix_username"]}" say "> Subscription: #{subscription_info["name"]}" # If the VCS type is SVN, we want it in all uppercase, otherwise just # capitilise it. if subscription_info["vcs_type"] == 'svn' say "> #{subscription_info["vcs_type"].upcase} URL: #{subscription_info["vcs_url"]}" else say "> #{subscription_info["vcs_type"].capitalize} URL: #{subscription_info["vcs_url"]}" end end end # Public: Provide an overview of the environments in your subscription. # # Returns the environment data in a pretty format. desc "list-environments ", "Provide an overview of the environments in your subscription." option :environment, :aliases => "-e" def list_environments(subscription) # If the environment option is set, just fetch a single environment. if options[:environment] subscription_envs = [options[:environment]] else subscription_envs = get_acquia_environments(subscription) end subscription_envs.each do |environment| env_info = acquia_api_call "sites/#{subscription}/envs/#{environment}" say say "> Host: #{env_info["ssh_host"]}" say "> Environment: #{env_info["name"]}" say "> Current release: #{env_info["vcs_path"]}" say "> DB clusters: #{env_info["db_clusters"].to_s unless env_info["db_clusters"].nil?}" say "> Default domain: #{env_info["default_domain"]}" end end # Public: Get server specs and information from an environment. # # This allows the ability to get all the server data from all server types # that are available within the subscription's environments. # # Returns server information on a per environment basis. desc "list-servers ", "Get a list of servers specifications for an environment." option :environment, :aliases => "-e" def list_servers(subscription) # Determine if we want just a single environment, or all of them at once. if options[:environment] subscription_envs = [options[:environment]] else subscription_envs = get_acquia_environments(subscription) end # Loop over each environment and get all the associated server data. subscription_envs.each do |environment| if options[:environment].nil? say say "Environment: #{environment}" end server_env = acquia_api_call "sites/#{subscription}/envs/#{environment}/servers" server_env.each do |server| say say "> Host: #{server["fqdn"]}" say "> EC2 region: #{server["ec2_region"]}" say "> Availability zone: #{server["ec2_availability_zone"]}" say "> EC2 instance type: #{server["ami_type"]}" # Show how many PHP processes this node can have. Note, this is only # available on the web servers. if server["services"] && server["services"]["php_max_procs"] say "> PHP max processes: #{server["services"]["php_max_procs"]}" end if server["services"] && server["services"]["status"] say "> Status: #{server["services"]["status"]}" end if server["services"] && server["services"]["web"] say "> Web status: #{server["services"]["web"]["status"]}" end # The state of varnish. if server["services"] && server["services"]["varnish"] say "> Varnish status: #{server["services"]["varnish"]["status"]}" end # Only load balancers will have the "external IP" property. if server["services"] && server["services"]["external_ip"] say "> External IP: #{server["services"]["external_ip"]}" end end end end # Public: Get information regarding the database instances. # # Within this method we have a few different options to get the information we # require. If just an environment is passed, only the names are returned. Pass # the environment param and the username, pasword, host, db cluster and # instance name are returned for each database available. Passing a database # name and the environment will only return that particular database. # # Returns database information. desc "list-databases ", "See information about the databases within a subscription." option :environment, :aliases => "-e" option :database, :aliases => "-d" def list_databases(subscription) # If we have both the database name and environment, only fetch a single # result. if options[:database] && options[:environment] database = acquia_api_call "sites/#{subscription}/envs/#{options[:environment]}/dbs/#{options[:database]}" say output_database_instance(database) return end # Fetch all the databases in a specific environment. if options[:environment] databases = acquia_api_call "sites/#{subscription}/envs/#{options[:environment]}/dbs" databases.each do |db| say say "#{db["name"]}" output_database_instance(db) end else subscription_envs = [options[:environment]] databases = acquia_api_call "sites/#{subscription}/dbs" say databases.each do |db| say "> #{db["name"]}" end end end # Public: List all backups for a database instance. # # Fetching all database backups for an instance is a pretty heavy call as the # data isn't restricted in any way by time, id's, etc. # # Returns a database backup listing. desc "list-database-backups ", "Get all backups for a database instance." def list_database_backups(subscription, environment, database) backups = acquia_api_call "sites/#{subscription}/envs/#{environment}/dbs/#{database}/backups" backups.each do |backup| say say "> ID: #{backup["id"]}" say "> MD5: #{backup["checksum"]}" say "> Type: #{backup["type"]}" say "> Path: #{backup["path"]}" say "> Link: #{backup["link"]}" say "> Started: #{Time.at(backup["started"].to_i)}" say "> Completed: #{Time.at(backup["completed"].to_i)}" end end # Public: Create a new database instance. # # Returns a success message upon creation. desc "add-database ", "Create a new database instance." def add_database(subscription, database) add_database = acquia_api_call "sites/#{subscription}/dbs", "POST", :db => "#{database}" success "A new database has been created." if add_database["id"] end # Public: Delete a database instance. # # Returns a status message based on the task completion. desc "delete-database ", "Remove all instances of a database." def delete_database(subscription, database) delete_db = acquia_api_call "sites/#{subscription}/dbs/#{database}?backup=0", "DELETE" success "Database has been successfully deleted." if delete_db["id"] end # Public: Copy a database from one environment to another. # # Returns the status message. desc "copy-database ", "Copy a database one from environment to another." def copy_database(subscription, database, source, destination) copy_database = acquia_api_call "sites/#{subscription}/dbs/#{database}/db-copy/#{source}/#{destination}", "POST" success "Database #{database} has been copied from #{source} to #{destination}." if copy_database["id"] end # Public: Restore a previous database backup to a site. # # Returns a status message. desc "restore-database-backup ", "Restore a database backup." def restore_database_backup(subscription, environment, database, backup_id) restore_db = acquia_api_call "sites/#{subscription}/envs/#{environment}/dbs/#{database}/backups/#{backup_id}/restore", "POST" success "Database backup #{backup_id} has been restored to #{database} in #{environment}." if restore_db["id"] end # Public: Show all the available domains for a subscription. # # Returns a list of the domains available. desc "list-domains ", "Show all available domains for a subscription." option :environment, :aliases => "-e" def list_domains(subscription) if options[:environment] subscription_envs = [options[:environment]] else subscription_envs = get_acquia_environments(subscription) end subscription_envs.each do |environment| domains = acquia_api_call "sites/#{subscription}/envs/#{environment}/domains" # Got top padding? if options[:environment] say else say say "Environment: #{environment}" end domains.each do |domain| say "> #{domain["name"]}" end end end # Public: Add a domain to an environment. # # Returns a status message on successful addition. desc "add-domain ", "Add a domain to an environment." def add_domain(subscription, environment, domain) add_domain = acquia_api_call "/sites/#{subscription}/envs/#{environment}/domains/#{domain}", "POST" success "Domain #{domain} has been successfully added to #{environment}." if add_domain["id"] end # Public: Remove a domain from an environment. # # Returns a status message on successful deletion. desc "delete-domain ", "Delete a domain from an environment." def delete_domain(subscription, environment, domain) delete_domain = acquia_api_call "/sites/#{subscription}/envs/#{environment}/domains/#{domain}", "DELETE" success "Domain #{domain} has been successfully deleted from #{environment}." if delete_domain["id"] end # Public: Clear a web cache on a domain. # # Send off a DELETE request to clear the web cache for a particular domain or # environment. # # Note: Clearing a whole environment is pretty performance heavy - use with # caution! # # Returns a status message form the purge request. desc "purge-domain ", "Clear the web cache of an environment or domain." option :domain, :aliases => "-d" def purge_domain(subscription, environment) domain = options[:domain] # If the domain is not defined, we are going to clear a whole environment. # This can have severe performance impacts on your environments. We need to # be sure this is definitely what you want to do. if domain purge_acquia_domain(subscription, environment, domain) else all_env_clear = ask "You are about to clear all domains in the #{environment} environment. Are you sure? (y/n)" # Last chance to bail out. if all_env_clear == "y" domains = acquia_api_call "sites/#{subscription}/envs/#{environment}/domains" domains.each do |domain| purge_acquia_domain("#{subscription}", "#{environment}", "#{domain["name"]}") end else info "Ok, no action has been taken." end end end # Public: Get all the SVN users. # # Returns a list of the SVN users. desc "list-svn-users ", "See all the SVN users on a subscription." def list_svn_users(subscription) svn_users = acquia_api_call "sites/#{subscription}/svnusers" svn_users.each do |user| say say "> ID: #{user["id"]}" say "> Name: #{user["username"]}" end end desc "delete-svn-user ", "Delete a SVN user." def delete_svn_user(subscription, userid) svn_user_removal = acquia_api_call "sites/#{subscription}/svnusers/#{userid}", "DELETE" success "#{userid} has been removed from the SVN users." if svn_user_removal["id"] end # Public: Get users on the subscription. # # Display a user listing with a truncated SSH key for security and ease of # use. # # Returns a list of users and truncated SSH keys. desc "list-ssh-users ", "Find out who has access and SSH keys." def list_ssh_users(subscription) users = acquia_api_call "sites/#{subscription}/sshkeys" users.each do |user| say say "> ID: #{user["id"]}" say "> Name: #{user["nickname"]}" say "> Key: #{truncate_ssh_key user["ssh_pub_key"]}" end end # Public: Delete a SSH key from the subscription. # # Returns a status message. desc "delete-ssh-user ", "Delete a SSH key from the subscription." def delete_ssh_user(subscription, id) delete_ssh_request = acquia_api_call "sites/#{subscription}/sshkeys/#{id}", "DELETE" success "SSH key #{id} has been successfully removed." if delete_ssh_request["id"] end # Public: Copy files from one environment to another. # # Returns a status message. desc "copy-files ", "Copy files from one environment to another." def copy_files(subscription, source, destination) file_copy = acquia_api_call "/sites/#{subscription}/files-copy/#{source}/#{target}", "POST" success "File copy from #{source} to #{destination} has started." if file_copy["id"] end # Public: Deploy a VCS branch or tag to an environment. # # NB: Unfortunately the API endpoint for this functionality is formed a little # differently to the others. It requires that the VCS path is appended to the # URL compared to plain old POST request with parameters as a payload. To # combat this, a pseudo request is made. It is a POST request at heart, just # named differently to allow this functionality to be separated out. # # Returns a status message string. desc "deploy-code ", "Deploy a specific VCS branch or tag to an environment." def deploy_code(subscription, environment, release) deploy_code = acquia_api_call "sites/#{subscription}/envs/#{environment}/code-deploy", "CODE-DEPLOY-POST", :release => "#{release}" success "#{release} has been deployed to #{environment}." if deploy_code["id"] end # Public: Show tasks for a subscription. # # Returns a listing of tasks for a subscription. desc "list-tasks ", "Display tasks associated with a subscription." option :count, :aliases => "-c" option :queue, :aliases => "-q" def list_tasks(subscription) all_tasks = acquia_api_call "sites/#{subscription}/tasks" tasks = [] # Fetch a single queue from the tasks list if the queue parameter is set # otherwise just add all the tasks. if options[:queue] all_tasks.each do |task| if task["queue"] == options[:queue] tasks << task end end else all_tasks.each do |task| tasks << task end end # If there is a count to return, restrict it to that required amount. if options[:count] && tasks.any? tasks = tasks.last(options[:count].to_i) end tasks.each do |task| output_task_item(task) end end end Acquia.startAcquia.start