crowbar/crowbar-core

View on GitHub
crowbar_framework/app/controllers/api/upgrade_controller.rb

Summary

Maintainability
F
3 days
Test Coverage
#
# Copyright 2016, SUSE Linux GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

class Api::UpgradeController < ApiController
  # disable upgrade API until upgrade to next version is implemented
  before_action(except: :show) do
    # skip filter if we're in the middle of upgrade from previos version
    unless File.exist?("/var/lib/crowbar/upgrade/8-to-9-upgrade-running")
      render json: {
        errors: {
          unexpected_error: {
            data: "Upgrade not yet supported in this version"
          }
        }
      }, status: :unprocessable_entity
    end
  end

  skip_before_filter :upgrade

  api :GET, "/api/upgrade", "Show the Upgrade progress"
  header "Accept", "application/vnd.crowbar.v2.0+json", required: true
  api_version "2.0"
  param :nodes, [true, false], desc: "Status of the nodes upgrade", required: false
  example '
  {
    "current_step": "admin",
    "current_substep": null,
    "current_nodes": null,
    "remaining_nodes": null,
    "upgraded_nodes": null,
    "steps": {
      "prechecks": {
        "status": "passed",
        "errors": {}
      },
      "prepare": {
        "status": "passed",
        "errors": {}
      },
      "backup_crowbar": {
        "status": "passed",
        "errors": {}
      },
      "repocheck_crowbar": {
        "status": "passed",
        "errors": {}
      },
      "admin": {
        "status": "failed",
        "errors": {
          "admin": {
            "data": "zypper dist-upgrade has failed with 8, check zypper logs",
            "help": "Failed to upgrade admin server. Refer to the error message in the response."
          }
        }
      },
      "database": {
        "status": "pending"
      },
      "repocheck_nodes": {
        "status": "pending"
      },
      "services": {
        "status": "pending"
      },
      "backup_openstack": {
        "status": "pending"
      },
      "nodes": {
        "status": "pending"
      },
      "finished": {
        "status": "pending"
      }
    }
  }
  '
  def show
    if params[:nodes]
      render json: Api::Upgrade.node_status
    else
      render json: Api::Upgrade.status
    end
  end

  api :POST, "/api/upgrade/prepare", "Prepare Crowbar Upgrade"
  header "Accept", "application/vnd.crowbar.v2.0+json", required: true
  api_version "2.0"
  error 422, "Failed to prepare nodes for Crowbar upgrade"
  def prepare
    ::Crowbar::UpgradeStatus.new.start_step(:prepare)

    Api::Upgrade.prepare(background: true)
    head :ok
  rescue ::Crowbar::Error::StartStepRunningError,
         ::Crowbar::Error::StartStepOrderError,
         ::Crowbar::Error::SaveUpgradeStatusError => e
    render json: {
      errors: {
        prepare: {
          data: e.message,
          help: I18n.t("api.upgrade.prepare.help.default")
        }
      }
    }, status: :unprocessable_entity
  end

  api :POST, "/api/upgrade/services", "Stop related services on all nodes during upgrade"
  header "Accept", "application/vnd.crowbar.v2.0+json", required: true
  api_version "2.0"
  error 422, "Failed to stop services on all nodes"
  def services
    ::Crowbar::UpgradeStatus.new.start_step(:services)
    Api::Upgrade.services
    head :ok
  rescue ::Crowbar::Error::StartStepRunningError,
         ::Crowbar::Error::StartStepOrderError,
         ::Crowbar::Error::SaveUpgradeStatusError => e
    render json: {
      errors: {
        services: {
          data: e.message,
          help: I18n.t("api.upgrade.services.help.default")
        }
      }
    }, status: :unprocessable_entity
  end

  api :POST, "/api/upgrade/nodes", "Initiate the node upgrade"
  api_version "2.0"
  param :component, String, desc: "Component to upgrade. 'all', 'controllers' or a node name",
                            required: true
  error 422, "Failed to upgrade nodes"
  # This is gonna initiate the upgrade of all nodes.
  # The method runs asynchronously, so there's a need to poll for the status and possible errors
  def nodes
    if params[:component]
      component = params[:component]
      upgrade_status = ::Crowbar::UpgradeStatus.new
      substep = upgrade_status.current_substep
      status = upgrade_status.current_substep_status
      if upgrade_status.compute_nodes_postponed? && component != "resume"
        raise ::Crowbar::Error::UpgradeError,
          "Upgrade is currently postponed. " \
          "It has to be resumed before proceeding with any other action."
      elsif ["resume", "postpone"].include? component
        if status == :failed
          raise ::Crowbar::Error::UpgradeError,
            "Previous step ended with a failure. Not possible to postpone or resume."
        end
        if component == "resume" && !upgrade_status.compute_nodes_postponed?
          raise ::Crowbar::Error::UpgradeError,
            "Upgrade is currently not postponed. No reason to resume."
        end
        if component == "postpone" && substep == :controller_nodes && status != :finished
          raise ::Crowbar::Error::UpgradeError,
            "Postponing is only possible when all controller nodes are already upgraded."
        end
      elsif ["all", "controllers"].include? component
        # When controller nodes have been upgraded previously,
        # whole 'nodes' step was not actually finished, just a substep.
        # It makes sense at this time to upgrade the rest with 'all'.
        unless (substep == :controller_nodes && status == :finished) ||
            # Other case when we don't want to start the step again is
            # when some compute node was already upgraded. Such case also leaves
            # the 'nodes' step as running, but user might want to upgrade all
            # remaining compute nodes by using 'all' argument.
            (substep == :compute_nodes && status == :node_finished)
          ::Crowbar::UpgradeStatus.new.start_step(:nodes)
        end
        ::Crowbar::UpgradeStatus.new.save_nodes_selected_for_upgrade(component)
      else
        # At this point params[:component] should be a node, if it is not,
        # raise an error.
        nodes_names = params[:component].split(/[\s,;]/)
        upgraded_nodes_names = []
        nodes_names.each do |name_or_alias|
          node = ::Node.find_node_by_name_or_alias(name_or_alias)
          if node.nil?
            raise ::Crowbar::Error::UpgradeError,
              "Component must be 'all', 'controllers', 'resume', "\
              "'postpone' or a node name(s). "\
              "No node with '#{name_or_alias}' name or alias was found. "
          end
          upgraded_nodes_names << name_or_alias if node.upgraded?
        end

        if (nodes_names - upgraded_nodes_names).empty?
          raise ::Crowbar::Error::UpgradeError,
            "All requested nodes are already upgraded."
        end
        if substep == :controller_nodes && status != :finished
          raise ::Crowbar::Error::UpgradeError.new(
            "Controller nodes must be upgraded first!"
          )
        end

        if upgrade_status.current_step == :nodes &&
            ::Crowbar::UpgradeStatus.new.passed?(:nodes)
          raise ::Crowbar::Error::UpgradeError.new(
            "Upgrade of nodes is already marked as finished."
          )
        end

        if substep == :compute_nodes && status == :running
          n = upgrade_status.progress[:current_nodes].first
          raise ::Crowbar::Error::UpgradeError.new(
            "Upgrade of node '#{n[:name]}' is already running. " \
            "Wait until it is finished before proceeding with next one."
          )
        end
        # If the 'nodes' step did not fail, it is still running and user can continue
        # with upgrading single compute node.
        if [:compute_nodes, :reload_nova, :run_online_migrations].include?(substep) &&
            status == :failed
          Rails.logger.info("Restarting the 'nodes' step after previous failure")
          ::Crowbar::UpgradeStatus.new.start_step(:nodes)
        end
        ::Crowbar::UpgradeStatus.new.save_nodes_selected_for_upgrade("compute")
      end
      Api::Upgrade.nodes component
      head :ok
    else
      render json: {
        errors: {
          nodes: {
            data: "No component parameter has been specified. " \
              "Pass 'all', 'controllers' or a node name(s) for upgrade actions. " \
              "Use 'postpone' for postponing upgrade of compute nodes. " \
              "Use 'resume' to resume postponed upgrade."
          }
        }
      }, status: :unprocessable_entity
    end
  rescue ::Crowbar::Error::UpgradeError,
         ::Crowbar::Error::StartStepRunningError,
         ::Crowbar::Error::StartStepOrderError,
         ::Crowbar::Error::SaveUpgradeStatusError => e
    render json: {
      errors: {
        nodes: {
          data: e.message,
          help: I18n.t("api.upgrade.nodes.help.default")
        }
      }
    }, status: :unprocessable_entity
  end

  api :GET, "/api/upgrade/prechecks", "Shows a sanity check in preparation for the upgrade"
  header "Accept", "application/vnd.crowbar.v2.0+json", required: true
  api_version "2.0"
  example '
  {
    "checks": {
      "network_checks": {
        "required": true,
        "passed": true,
        "errors": {}
      },
      "cloud_healthy": {
        "required": true,
        "passed": true,
        "errors": {}
      },
      "maintenance_updates_installed": {
        "required": true,
        "passed": false,
        "errors": {
          "maintenance_updates_installed": {
            "data": [
              "ZYPPER_EXIT_INF_UPDATE_NEEDED: patches available for installation."
            ],
            "help": "make sure maintenance updates are installed"
          }
        }
      },
      "compute_status": {
        "required": false,
        "passed": true,
        "errors": {}
      },
      "ceph_healthy": {
        "required": true,
        "passed": true,
        "errors": {}
      },
      "ha_configured": {
        "required": false,
        "passed": true,
        "errors": {}
      },
      "clusters_healthy": {
        "required": true,
        "passed": true,
        "errors": {}
      }
    },
    "best_method": "non-disruptive"
  }
  '
  def prechecks
    render json: Api::Upgrade.checks
  rescue Crowbar::Error::UpgradeError => e
    render json: {
      errors: {
        prechecks: {
          data: e.message,
          help: I18n.t("api.upgrade.prechecks.help.default")
        }
      }
    }, status: :unprocessable_entity
  rescue StandardError => e
    log_exception(e)
    render json: {
      errors: {
        prechecks: {
          data: e.message,
          help: I18n.t("api.upgrade.prechecks.help.default")
        }
      }
    }, status: :unprocessable_entity
  end

  api :POST, "/api/upgrade/cancel", "Cancel the upgrade process by setting the nodes back to ready"
  header "Accept", "application/vnd.crowbar.v2.0+json", required: true
  api_version "2.0"
  error 422, "Failed to cancel the upgrade process"
  error 423, "Not possible to cancel the upgrade process at this stage"
  def cancel
    if Api::Upgrade.cancel
      head :ok
    else
      render json: {
        errors: {
          cancel: {
            data: I18n.t("api.upgrade.cancel.failed"),
            help: I18n.t("api.upgrade.cancel.help.default")
          }
        }
      }, status: :unprocessable_entity
    end
  rescue Crowbar::Error::Upgrade::CancelError => e
    render json: {
      errors: {
        cancel: {
          data: e.message,
          help: I18n.t("api.upgrade.cancel.help.not_allowed")
        }
      }
    }, status: :locked
  rescue StandardError => e
    log_exception(e)
    render json: {
      errors: {
        cancel: {
          data: e.message,
          help: I18n.t("api.upgrade.cancel.help.default")
        }
      }
    }, status: :unprocessable_entity
  end

  api :GET, "/api/upgrade/noderepocheck", "Check for missing node repositories"
  header "Accept", "application/vnd.crowbar.v2.0+json", required: true
  api_version "2.0"
  example '
  {
    "ha": {
      "available": true,
      "repos": [
        "SLE12-SP3-HA-Pool",
        "SLE12-SP3-HA-Updates"
      ],
      "errors": {
      }
    },
    "os": {
      "available": false,
      "repos": [
        "SLES12-SP3-Pool",
        "SLES12-SP3-Updates"
      ],
      "errors": {
        "missing": {
          "x86_64": [
            "SLES12-SP3-Pool"
          ]
        },
        "inactive": {
          "x86_64": [
            "SLES12-SP3-Pool"
          ]
        }
      }
    },
    "openstack": {
      "available": true,
      "repos": [
        "SUSE-OpenStack-Cloud-Crowbar-8-Pool",
        "SUSE-OpenStack-Cloud-Crowbar-8-Updates"
      ],
      "errors": {
      }
    }
  }
  '
  def noderepocheck
    render json: Api::Upgrade.noderepocheck
  rescue Crowbar::Error::UpgradeError => e
    render json: {
      errors: {
        repocheck_nodes: {
          data: e.message,
          help: I18n.t("api.upgrade.noderepocheck.help.default")
        }
      }
    }, status: :unprocessable_entity
  end

  api :GET, "/api/upgrade/adminrepocheck",
    "Sanity check for Crowbar server repositories"
  header "Accept", "application/vnd.crowbar.v2.0+json", required: true
  api_version "2.0"
  example '
  {
    "os": {
      "available": true,
      "repos": {}
    },
    "openstack": {
      "available": false,
      "repos": [
        "SUSE-OpenStack-Cloud-Crowbar-8-Pool",
        "SUSE-OpenStack-Cloud-Crowbar-8-Updates"
      ],
      "errors":
        "x86_64": {
          "missing": [
            "SUSE-OpenStack-Cloud-Crowbar-8-Pool",
            "SUSE-OpenStack-Cloud-Crowbar-8-Updates"
          ]
        }
      }
    }
  }
  '
  error 503, "zypper is locked"
  def adminrepocheck
    check = Api::Upgrade.adminrepocheck

    if check.key?(:error)
      render json: {
        errors: {
          repocheck_crowbar: {
            data: check[:error],
            help: I18n.t("api.upgrade.adminrepocheck.help.default")
          }
        }
      }, status: check[:status]
    else
      render json: check
    end
  rescue Crowbar::Error::UpgradeError => e
    render json: {
      errors: {
        repocheck_crowbar: {
          data: e.message,
          help: I18n.t("api.upgrade.adminrepocheck.help.default")
        }
      }
    }, status: :unprocessable_entity
  rescue StandardError => e
    log_exception(e)
    render json: {
      errors: {
        repocheck_crowbar: {
          data: e.message,
          help: I18n.t("api.upgrade.adminrepocheck.help.default")
        }
      }
    }, status: :unprocessable_entity
  end

  api :POST, "/api/upgrade/adminbackup", "Create a backup"
  header "Accept", "application/vnd.crowbar.v2.0+json", required: true
  api_version "2.0"
  param :backup, Hash, desc: "Backup info", required: true do
    param :name, String, desc: "Name of the backup", required: true
  end
  example '
  {
    "id": 1,
    "name": "testbackup",
    "version": "4.0",
    "size": 76815,
    "created_at": "2016-09-27T06:05:10.208Z",
    "updated_at": "2016-09-27T06:05:10.208Z",
    "migration_level": 20160819142156
  }
  '
  error 422, "Failed to save backup, error details are provided in the response"
  def adminbackup
    # FIXME: move this logic into the model
    upgrade_status = ::Crowbar::UpgradeStatus.new
    upgrade_status.start_step(:backup_crowbar)
    @backup = Api::Backup.new(backup_params)

    if @backup.save
      upgrade_status.end_step
      render json: @backup, status: :ok
    else
      upgrade_status.end_step(
        false,
        backup_crowbar: @backup.errors.full_messages.first
      )
      render json: {
        errors: {
          backup_crowbar: {
            data: @backup.errors.full_messages,
            help: I18n.t("api.upgrade.adminbackup.help.default")
          }
        }
      }, status: :unprocessable_entity
    end
  rescue Crowbar::Error::StartStepRunningError,
         Crowbar::Error::StartStepOrderError,
         Crowbar::Error::EndStepRunningError,
         Crowbar::Error::SaveUpgradeStatusError => e
    render json: {
      errors: {
        backup_crowbar: {
          data: e.message,
          help: I18n.t("api.upgrade.adminbackup.help.default")
        }
      }
    }, status: :unprocessable_entity
  rescue StandardError => e
    ::Crowbar::UpgradeStatus.new.end_step(
      false,
      backup_crowbar: {
        data: e.message,
        help: "Crowbar has failed. Check /var/log/crowbar/production.log for details."
      }
    )
    raise e
  ensure
    @backup.cleanup unless @backup.nil?
  end

  api :POST, "/api/upgrade/openstackbackup", "Create a backup of Openstack"
  api_version "2.0"
  error 422, "Failed to save backup, error details are provided in the response"
  def openstackbackup
    ::Crowbar::UpgradeStatus.new.start_step(:backup_openstack)
    Api::Upgrade.openstackbackup
    head :ok
  rescue ::Crowbar::Error::StartStepRunningError,
         ::Crowbar::Error::StartStepOrderError,
         ::Crowbar::Error::SaveUpgradeStatusError => e
    render json: {
      errors: {
        backup_openstack: {
          data: e.message,
          help: "Please refer to the error message in the response."
        }
      }
    }, status: :unprocessable_entity
  end

  api :GET, "/api/crowbar/mode", "Current upgrade mode"
  header "Accept", "application/vnd.crowbar.v2.0+json", required: true
  api :POST, "/api/upgrade/mode", "Switch upgrade mode"
  api_version "2.0"
  error 422, "Failed to save upgrade mode"
  def mode
    if request.post?
      Api::Upgrade.upgrade_mode = params[:mode]
      render json: {}, status: :ok
    else
      render json: {
        mode: Api::Upgrade.upgrade_mode
      }
    end
  rescue ::Crowbar::Error::SaveUpgradeModeError,
         ::Crowbar::Error::SaveUpgradeStatusError,
         ::Crowbar::Error::UpgradeError => e
    render json: {
      errors: {
        mode: {
          data: e.message
        }
      }
    }, status: :unprocessable_entity
  end

  protected

  api :POST, "/api/upgrade/new",
    "Initialization of Crowbar during upgrade with creation of a new database.
    NOTE: It is only possible to use this endpoint during the stage where crowbar-init is running."
  header "Accept", "application/vnd.crowbar.v2.0+json", required: true
  param :username, /(?=^.{4,63}$)(?=^[a-zA-Z0-9_]*$)/,
    desc: "Username
      Min length: 4
      Max length: 63
      Only alphanumeric characters or underscores
      Must begin with a letter [a-zA-Z] or underscore",
    required: true
  param :password, /(?=^.{4,63}$)(?=^[a-zA-Z0-9_]*$)(?=[a-zA-Z0-9_$&+,:;=?@#|'<>.^*()%!-]*$)/,
    desc: "Password
      Min length: 4
      Max length: 63
      Alphanumeric and special characters
      Must begin with any alphanumeric character or underscore",
    required: true
  api_version "2.0"
  example '
  {
    "database_setup": {
      "success": true
    },
    "database_migration": {
      "success": true
    },
    "schema_migration": {
      "success": true
    },
    "crowbar_init": {
      "success": false,
      "body": {
        "error": "crowbar_init: Failed to stop crowbar-init.service"
      }
    }
  }
  '
  error 422, "Failed to initialize Crowbar, details are provided in the response hash"
  def dummy_crowbar_init_api_upgrade_new
    # empty method to document crowbar-init's upgrade related API endpoints
  end

  api :POST, "/api/upgrade/connect",
    "Initialization of Crowbar during upgrade with connection to an existing database.
    NOTE: It is only possible to use this endpoint during the stage where crowbar-init is running."
  header "Accept", "application/vnd.crowbar.v2.0+json", required: true
  param :username, /(?=^.{4,63}$)(?=^[a-zA-Z0-9_]*$)/,
    desc: "External database username
      Min length: 4
      Max length: 63
      Only alphanumeric characters and/or underscores
      Must begin with a letter [a-zA-Z] or underscore", required: true
  param :password, /(?=^.{4,63}$)(?=^[a-zA-Z0-9_]*$)(?=[a-zA-Z0-9_$&+,:;=?@#|'<>.^*()%!-]*$)/,
    desc: "External database password
      Min length: 4
      Max length: 63
      Alphanumeric and special characters
      Must begin with any alphanumeric character or underscore",
    required: true
  param :database, /(?=^.{4,253}$)(?=^[a-zA-Z0-9_]*$)(?=[a-zA-Z0-9_$&+,:;=?@#|'<>.^*()%!-]*$)/,
    desc: "Database name
      Min length: 4
      Max length: 63
      Alphanumeric characters and underscores
      Must begin with any alphanumeric character or underscore",
    required: true
  param :host, /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/,
    desc: "External database host, Ipv4 or Hostname
      Min length: 4
      Max length: 253
      Numbers and period characters (only IPv4)
      Hostnames/FQDNs:
       alphanumeric characters, dots and hyphens
       cannot start/end with digits or hyphen",
    required: true
  param :port, /(?=^.{1,5}$)(?=^[0-9]*$)/,
    desc: "External database port
      Min length: 1
      Max length: 5
      Only numbers",
    required: true
  api_version "2.0"
  example '
  {
    "database_setup": {
      "success": true
    },
    "database_migration": {
      "success": true
    },
    "schema_migration": {
      "success": true
    },
    "crowbar_init": {
      "success": false,
      "body": {
        "error": "crowbar_init: Failed to stop crowbar-init.service"
      }
    }
  }
  '
  error 406, "Connection to external database failed. Possible errors can be:
    host not found, incorrect port, wrong credentials, wrong database name"
  error 422, "Failed to initialize Crowbar, details are provided in the response hash"
  def dummy_crowbar_init_api_upgrade_connect
    # empty method to document crowbar-init's upgrade related API endpoints
  end

  def backup_params
    params.require(:backup).permit(:name)
  end
end