diff --git a/.gitignore b/.gitignore index c676868..e06b470 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ /ec2/ /sites/ node_modules -/sites.json/ +/sites.json diff --git a/config.mjs b/config.mjs new file mode 100644 index 0000000..2aa5c9f --- /dev/null +++ b/config.mjs @@ -0,0 +1,63 @@ +import { existsSync, readFileSync, writeFileSync } from 'fs'; + +export const validateHost = (host) => { + if (/^(http|https):\/\//.test(host)) { + console.log('Scheme (http:// or https://) for host must be omitted.'); + process.exit(1); + } +}; + +export const validateConfig = (config) => { + if ( + !config || + config === true || + Array.isArray(config) || + config.length !== undefined || + typeof config === "number" + ) { + console.log("Configuration file is not valid."); + process.exit(1); + } + + if (!config.ec2) { + console.log("'ec2' is required."); + process.exit(1); + } + + if (!config.ec2.host) { + console.log("'ec2.host' is required."); + process.exit(1); + } + + validateHost(config.ec2.host); +}; + +export const getConfigPath = () => { + const args = process.argv.slice(2); + const rawconfigf = args[0] ? args[0] : null; + return rawconfigf; +} + +export const getConfig = () => { + const args = process.argv.slice(2); + const rawconfigf = args[0] ? args[0] : null; + + if (rawconfigf === null) { + console.log('A path to a valid JSON file is required as the first argument.'); + process.exit(1); + } + + if (!existsSync(rawconfigf)) { + console.log(`${rawconfigf} doesnt exist`); + process.exit(1); + } + + const rawconfig = JSON.parse(readFileSync(rawconfigf, 'utf8')); + validateConfig(rawconfig); + + return rawconfig; +} + +export const updateConfigFile = (config) => { + writeFileSync(getConfigPath(), JSON.stringify(config, null, 2)); +} diff --git a/ec2-init.mjs b/ec2-init.mjs new file mode 100644 index 0000000..2765722 --- /dev/null +++ b/ec2-init.mjs @@ -0,0 +1,42 @@ +import { exec } from 'child_process'; +import _ from 'lodash'; +import { updateConfigFile } from './config.mjs'; +import { SSH, shell, genPwd } from "./utils.mjs"; + +/** + * Initialize kusanagi (Nginx, MySQL, etc) + */ +export const ec2Init = (config, callback) => { + const client = new SSH(config); + + client.sshCentos("sudo dnf update -y"); + + exec(`ssh -i ${config.ec2.centos.pem} -t centos@${config.ec2.host} "sudo reboot"`, () => { + console.log('Rebooted. Waiting for instance to come back online...') + shell('sleep 60'); + + const kusanagiPwd = genPwd(); + const dbpwd = genPwd(); + + const outjson = _.cloneDeep(config); + + client.sshCentos( + `sudo su - -c 'kusanagi init --tz Asia/Tokyo --lang ja --keyboard en --passwd \"${kusanagiPwd}\" --nophrase --dbrootpass \"${dbpwd}\" --nginx121 --php74 --mariadb10.5'` + ); + + outjson.ec2.kusanagi.userpwd = kusanagiPwd; + outjson.ec2.mysqlRootPass = dbpwd; + updateConfigFile(outjson); + + client.sshCentos( + 'sudo mv /root/kusanagi.pem /home/centos/ && sudo chown centos:centos /home/centos/kusanagi.pem' + ); + + // download kusanagi SSH private key + client.downloadCentos("/home/centos/kusanagi.pem", "./"); + shell(`mv -f ./kusanagi.pem ${config.ec2.kusanagi.pem}`); + shell(`chmod 400 ${config.ec2.kusanagi.pem}`); + + callback(); + }); +}; diff --git a/ec2-init.run.mjs b/ec2-init.run.mjs new file mode 100755 index 0000000..5ec6030 --- /dev/null +++ b/ec2-init.run.mjs @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import { getConfig } from './config.mjs'; +import { ec2Init } from './ec2-init.mjs'; + +const config = getConfig(); +ec2Init(config); diff --git a/examples/sites.json b/examples/sites.json index 62c1d7d..6dbab98 100644 --- a/examples/sites.json +++ b/examples/sites.json @@ -1,21 +1,24 @@ { - "host": "ec2-11-111-11-11.ap-northeast-3.compute.amazonaws.com", - "kusanagi": { - "password": "somepassword" + "domain": "mydomain-name.com", + "ec2": { + "host": "ec2-11-111-11-11.ap-northeast-3.compute.amazonaws.com", + "centos": { + "pem": "./ec2/aivec.co.jp-centos.pem" + }, + "kusanagi": { + "pem": "./ec2/aivec.co.jp-kusanagi.pem" + } }, "rootsite": { - "host": "my.site.com", "dbname": "mydbname", "dbuser": "mydbuser", - "dbpass": "mydbpass", "wpuser": "mywpuser" }, "subsites": [ { - "host": "my.site.com/subsite", + "name": "subsite-folder-name", "dbname": "mysubdbname", "dbuser": "mysubdbuser", - "dbpass": "mysubdbpass", "wpuser": "mysubwpuser" } ] diff --git a/nginx-configure.mjs b/nginx-configure.mjs new file mode 100644 index 0000000..41e8429 --- /dev/null +++ b/nginx-configure.mjs @@ -0,0 +1,21 @@ +import { SSH } from "./utils.mjs"; + +/** + * Configures Nginx to properly route subdirs to their corresponding WordPress install + */ +export const nginxConfigure = (config) => { + const client = new SSH(config); + + const commands = []; + commands.push("cd /etc/opt/kusanagi/nginx/conf.d;"); + config.subsites.forEach((subsite) => { + commands.push( + `echo \\"upstream ${subsite.name} { server 127.0.0.1; }\\" > ${subsite.name}.upstream.conf;` + ); + + client.sshCentos(`sudo sed -i '/root\\\s\\/home\\/kusanagi\\/${config.rootsite.name}\\/DocumentRoot/a "location ^~ /${subsite.name}/ { \\nproxy_pass http://${subsite.name}/ \\n}"' /etc/opt/kusanagi/nginx/conf.d/${config.rootsite.name}.conf`); + }); + commands.push("kusanagi nginx --reload;"); + + client.sshCentos(`sudo su - -c '${commands.join(" ")}'`); +}; diff --git a/nginx-configure.run.mjs b/nginx-configure.run.mjs new file mode 100755 index 0000000..2c9a670 --- /dev/null +++ b/nginx-configure.run.mjs @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import { getConfig } from './config.mjs'; +import { nginxConfigure } from './nginx-configure.mjs'; + +const config = getConfig(); +nginxConfigure(config); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..3969f82 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,39 @@ +{ + "name": "@aivec/wp-migrate-to-ec2-kusanagi9", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@aivec/wp-migrate-to-ec2-kusanagi9", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "generate-password": "^1.7.0", + "lodash": "^4.17.21" + } + }, + "node_modules/generate-password": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/generate-password/-/generate-password-1.7.0.tgz", + "integrity": "sha512-WPCtlfy0jexf7W5IbwxGUgpIDvsZIohbI2DAq2Q6TSlKKis+G4GT9sxvPxrZUGL8kP6WUXMWNqYnxY6DDKAdFA==" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + } + }, + "dependencies": { + "generate-password": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/generate-password/-/generate-password-1.7.0.tgz", + "integrity": "sha512-WPCtlfy0jexf7W5IbwxGUgpIDvsZIohbI2DAq2Q6TSlKKis+G4GT9sxvPxrZUGL8kP6WUXMWNqYnxY6DDKAdFA==" + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + } + } +} diff --git a/package.json b/package.json index b721d37..e3c3084 100644 --- a/package.json +++ b/package.json @@ -7,5 +7,9 @@ "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", - "license": "ISC" + "license": "ISC", + "dependencies": { + "generate-password": "^1.7.0", + "lodash": "^4.17.21" + } } diff --git a/provision.mjs b/provision.mjs new file mode 100644 index 0000000..2ec3acb --- /dev/null +++ b/provision.mjs @@ -0,0 +1,34 @@ +import _ from "lodash"; +import { generate } from "generate-password"; +import { updateConfigFile } from "./config.mjs"; +import { SSH, genPwd } from "./utils.mjs"; + +export const provision = (config) => { + const utils = new SSH(config); + + const dbpass = genPwd(); + + const outjson = _.cloneDeep(config); + + utils.sshKusanagi( + `kusanagi provision --wp --wplang ja --fqdn ${config.ec2.host} --no-email --dbname ${config.rootsite.dbname} --dbuser ${config.rootsite.dbuser} --dbpass '${dbpass}' '${config.rootsite.name}'` + ); + + outjson.rootsite.dbpass = dbpass; + outjson.rootsite.fullname = config.rootsite.name; + outjson.rootsite.url = config.ec2.host; + updateConfigFile(outjson); + + config.subsites.forEach((subsite, i) => { + const fullname = `${config.rootsite.name}-${subsite.name}`; + const subdbpass = genPwd(); + utils.sshKusanagi( + `kusanagi provision --wp --wplang ja --fqdn ${subsite.name} --no-email --dbname ${subsite.dbname} --dbuser ${subsite.dbuser} --dbpass '${subdbpass}' '${fullname}'` + ); + + outjson.subsites[i].dbpass = subdbpass; + outjson.subsites[i].fullname = fullname; + outjson.subsites[i].url = `${config.ec2.host}/${subsite.name}`; + updateConfigFile(outjson); + }); +}; diff --git a/provision.run.mjs b/provision.run.mjs new file mode 100755 index 0000000..775535b --- /dev/null +++ b/provision.run.mjs @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import { getConfig } from './config.mjs'; +import { provision } from './provision.mjs'; + +const config = getConfig(); +provision(config); diff --git a/run.mjs b/run.mjs index ec54f85..ab69e87 100755 --- a/run.mjs +++ b/run.mjs @@ -1,21 +1,11 @@ #!/usr/bin/env node -import { existsSync, readFileSync } from 'fs'; -import { validateConfig } from './validate.mjs'; - -const args = process.argv.slice(2); -const rawconfigf = args[0] ? args[0] : null; - -if (rawconfigf === null) { - console.log('A path to a valid JSON file is required as the first argument.'); - process.exit(1); -} - -if (!existsSync(rawconfigf)) { - console.log(`${rawconfigf} doesnt exist`); - process.exit(1); -} - -const rawconfig = JSON.parse(readFileSync(rawconfigf, 'utf8')); -console.log(rawconfig); - -validateConfig({ host: 'mysite.com'}); +import { getConfig } from './config.mjs'; +import { ec2Init } from './ec2-init.mjs'; +import { provision } from './provision.mjs'; +import { wpInstall } from './wp-install.mjs'; + +const config = getConfig(); +ec2Init(config, () => { + provision(config); + wpInstall(config); +}); diff --git a/utils.mjs b/utils.mjs new file mode 100644 index 0000000..e6302de --- /dev/null +++ b/utils.mjs @@ -0,0 +1,52 @@ +import { generate } from "generate-password"; +import { execSync } from "child_process"; + +export const shell = (command) => execSync(command, { + shell: '/bin/bash', + stdio: 'inherit', +}); + +export const genPwd = () => { + return generate({ + length: 20, + symbols: "%_#", + numbers: true, + strict: true, + }); +} + +export class SSH { + finalc = ""; + + constructor(finalc) { + this.finalc = finalc; + } + + sshCentos(command) { + shell(`chmod 400 ${this.finalc.ec2.centos.pem}`); + shell( + `ssh -i ${this.finalc.ec2.centos.pem} -t centos@${this.finalc.ec2.host} "${command}"` + ); + } + + sshKusanagi(command) { + shell(`chmod 400 ${this.finalc.ec2.kusanagi.pem}`); + shell( + `ssh -i ${this.finalc.ec2.kusanagi.pem} -t kusanagi@${this.finalc.ec2.host} "${command}"` + ); + } + + uploadKusanagi(localpath, remotepath) { + shell(`chmod 400 ${this.finalc.ec2.kusanagi.pem}`); + shell( + `scp -i ${this.finalc.ec2.kusanagi.pem} ${localpath} kusanagi@${this.finalc.ec2.host}:${remotepath}` + ); + } + + downloadCentos(remotepath, localpath) { + shell(`chmod 400 ${this.finalc.ec2.centos.pem}`); + shell( + `scp -i ${this.finalc.ec2.centos.pem} centos@${this.finalc.ec2.host}:${remotepath} ${localpath}` + ); + } +} diff --git a/validate.mjs b/validate.mjs deleted file mode 100644 index c96a422..0000000 --- a/validate.mjs +++ /dev/null @@ -1,26 +0,0 @@ -export const validateHost = (host) => { - if (/^(http|https):\/\//.test(host)) { - console.log('Scheme (http:// or https://) for host must be omitted.'); - process.exit(1); - } -}; - -export const validateConfig = (config) => { - if ( - !config || - config === true || - Array.isArray(config) || - config.length !== undefined || - typeof config === "number" - ) { - console.log("Configuration file is not valid."); - process.exit(1); - } - - if (!config.host) { - console.log("'host' is required."); - process.exit(1); - } - - validateHost(config.host); -}; diff --git a/wp-install.mjs b/wp-install.mjs new file mode 100755 index 0000000..0462cbd --- /dev/null +++ b/wp-install.mjs @@ -0,0 +1,106 @@ +import _ from "lodash"; +import { SSH } from "./utils.mjs"; + +export const getFnameFromZip = (zipFile) => { + let pieces = zipFile.split("/"); + let lastpart = pieces[pieces.length - 1]; + pieces = lastpart.split(".zip"); + pieces.pop(); + return pieces.join(""); +}; + +const prepareFiles = (config, site) => { + const client = new SSH(config); + + let dumpFile = null; + let wpContent = null; + if (site.dumpFileZip) { + dumpFile = getFnameFromZip(site.dumpFileZip); + } + if (site.wpContentZip) { + wpContent = getFnameFromZip(site.wpContentZip); + } + + const commands = []; + commands.push(`cd /home/kusanagi/${site.fullname}/DocumentRoot;`); + if (site.dumpFileZip) { + commands.push(`cd zips;`); + commands.push(`unzip -o ${dumpFile}.zip;`); + commands.push(`mv ${dumpFile}.sql ../;`); + commands.push(`cd ../;`); + } + if (site.wpContentZip) { + commands.push(`cd zips;`); + commands.push(`unzip -o ${wpContent}.zip;`); + commands.push(`cp -af ${wpContent}/* ../wp-content/;`); + commands.push(`cd ../;`); + } + commands.push(`rm -rf zips;`); + + client.sshKusanagi(commands.join(" ")); +}; + +const performInstall = (config, site) => { + const client = new SSH(config); + + let dumpFile = null; + if (site.dumpFileZip) { + dumpFile = getFnameFromZip(site.dumpFileZip); + } + + const commands = []; + commands.push(`cd /home/kusanagi/${site.fullname}/DocumentRoot;`); + commands.push( + `wp config create --dbname=${site.dbname} --dbuser=${site.dbuser} --dbpass=${site.dbpass};` + ); + commands.push(`wp db drop --yes;`); + commands.push(`wp db create;`); + if (site.dumpFileZip) { + commands.push(`wp db import ${dumpFile}.sql;`); + commands.push(`wp option get home | xargs -I{} wp search-replace {} http://${site.url};`); + commands.push(`wp option get siteurl | xargs -I{} wp search-replace {} http://${site.url};`); + commands.push(`wp core update-db;`); + } + + client.sshKusanagi(commands.join(' ')); +}; + +const updatePermissions = (config, site) => { + const client = new SSH(config); + + client.sshCentos(` + cd /home/kusanagi/${site.fullname}/DocumentRoot; + sudo chown httpd:www wp-config.php; + sudo chmod 666 wp-config.php; + `); +}; + +export const wpInstall = (config) => { + const client = new SSH(config); + + [config.rootsite, ...config.subsites].forEach((site) => { + if (site.dumpFileZip || site.wpContentZip) { + client.sshKusanagi( + `mkdir -p /home/kusanagi/${site.fullname}/DocumentRoot/zips` + ); + } + + if (site.dumpFileZip) { + client.uploadKusanagi( + site.dumpFileZip, + `/home/kusanagi/${site.fullname}/DocumentRoot/zips/` + ); + } + + if (site.wpContentZip) { + client.uploadKusanagi( + site.wpContentZip, + `/home/kusanagi/${site.fullname}/DocumentRoot/zips/` + ); + } + + prepareFiles(config, site); + performInstall(config, site); + updatePermissions(config, site); + }); +}; diff --git a/wp-install.run.mjs b/wp-install.run.mjs new file mode 100755 index 0000000..b304379 --- /dev/null +++ b/wp-install.run.mjs @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import { getConfig } from './config.mjs'; +import { wpInstall } from './wp-install.mjs'; + +const config = getConfig(); +wpInstall(config);