Half-Elf on Tech

Thoughts From a Professional Lesbian

Tag: command line

  • CLI Like a Webhost

    CLI Like a Webhost

    For the last ten years, I worked for DreamHost, which meant I had access to a lot of awesome commands that everyone ran to diagnose things.

    Well now I’m gone and I’m still a webadmin for my domains. And I have, as you all know, a weird guy who keeps going after me. I also have been running fandom sites for longer than WordPress has existed. I’ve had to learn a lot of tricks to sort out ‘Is this person so-and-so again?!’

    Now… I’m going to tell you a secret. You ready? Okay, most of those scripts hosts run? They’re just cleaned up shell commands you run on the server via command line (aka command line interface aka cli). And those commands? They’re actually pretty common, well known, and public.

    So here are some of the ones I use and why!

    Before You Begin…

    I have to step back a moment.

    Do you know where your log files are? DreamHost posted in their KB how you do that, but you will want to check your hosts:

    There are three caveats, and I know one is weird.

    1. Logs rotate

    Server space matters, so logs are regularly deleted to prevent your data from killing things.

    Right now I see this:

    -rw-r--r-- 1 root      root      3.5M Sep 16 09:08 access.log
    lrwxrwxrwx 1 root      root        21 Sep 16 00:49 access.log.0 -> access.log.2022-09-15
    -rw-r--r-- 1 username  server    1.6M Sep 12 00:51 access.log.2022-09-11.gz
    -rw-r--r-- 1 username  server    1.4M Sep 13 00:54 access.log.2022-09-12.gz
    -rw-r--r-- 1 username  server    1.6M Sep 14 00:11 access.log.2022-09-13.gz
    -rw-r--r-- 1 root      root      9.7M Sep 15 00:21 access.log.2022-09-14
    -rw-r--r-- 1 root      root       11M Sep 16 00:49 access.log.2022-09-15
    

    Tomorrow I’ll loose the 9-11 log.

    2. You need to know what your logs look like

    Every host tweaks the format of apache logs in a different way. You’ll see I use things like print $1 in my code, and for me I know that means “$1 is the IP address.” But that may not be what your host does.

    Look at the logs:

    192.0.114.84 - - [16/Sep/2022:00:49:05 -0700] "GET /wp-content/uploads/2019/10/Pure.jpg HTTP/1.1" 200 257552 "-" "Photon/1.0"

    And then count things. IP is , URL is , and so on.

    It can be a pain so please feel free to experiment and mess with it to get exactly what you want.

    3. You may need to use http logs for everything

    This is specific to DreamPRESS (the managed WP hosting) and is the weird thing, you always have to use the http folder even if you use https.

    Why? Well that has to do with how the server processes traffic. DreamPress (as of the time of this post) uses Varnish to cache and Nginx as an SSL proxy. That means when you go to https://example.com the server has nginx check the HTTPS stuff and passes it to Apache, which runs HTTP. Those logs are your apache logs, not your Nginx ones.

    Can you view the Nginx logs? Not at this time. Also they really are pass-throughs, so you’re not missing much. If you think you are, please open a ticket and tell them what you’re looking for in specific. Those help-desk folks are awesome, but the more clear you are about exactly what you’re looking for, the better help you get.

    Okay! On with the show!

    Top IPs

    Sometimes your site is running super slow and you want to know “Who the heck is hitting my site so much!?”

    awk '{ print $1}' access.log | sort | uniq -c | sort -nr | head -n 10
    

    This command will list the top 10 IPs that hit your site. I find this one super helpful when used in conjunction with an IP lookup service like IPQualityScore, because it tells me sometimes “Hey, did you know Amazon’s bots are hitting the heck out of your site!?”

    You can change that 10 to whatever number of top IPs you want to look for. That tends to be enough for me.

    If you know you have a lot of ‘self’ lookups (like you wrote something that has your server do a thing) you’ll want to try something like this to exclude them:

    awk '{print $1}' access.log | grep -ivE "(127.0.0.1|192.168.100.)" | sort | uniq -c | sort -rn | head -10
    

    Sometimes you just want to know what pages are being hit, right?

    Remember how I said you actually need to know what your log looks like? For me, $7 is the 7th ‘item’ in my access log:

    192.0.114.84 - - [16/Sep/2022:00:49:05 -0700] "GET /wp-content/uploads/2019/10/Pure.jpg HTTP/1.1" 200 257552 "-" "Photon/1.0"

    Counting is weird, I know, but the 7th is ‘/wp-content/uploads…’ so I know that the command has to use $7. BTW Photon there just means I use WordPress’s image stuff via Jetpack.

    awk '{print $7}' access.log | grep -ivE '(mod_status|favico|crossdomain|alive.txt)' | grep -ivE '(.gif|.jpg|.png|.js|.css)' | \
     sed 's/\/$//g' | sort | \
     uniq -c | sort -rn | head -25
    

    That returns a unique list:

        862 /xmlrpc.php
        539 /wp-admin/admin-ajax.php
        382 /wp-login.php
         75 /wp-cron.php?doing_wp_cron

    And it’s not a shock those are the high hits. Nice try folks. I use things to protect me. But before we get into that…

    IPs Hitting a Specific Page

    Now let’s say you’re trying to figure out what numb nut is hitting a specific page on your site! For example, I have a page called “electric-boogaloo” and I’m pretty sure someone specific is hammering that page. I’ll do this:

    awk -F'[ "]+' '$7 == "/electric-boogaloo/" { ipcount[$1]++ }
        END { for (i in ipcount) {
            printf "%15s - %d\n", i, ipcount[i] } }' access.log
    

    That spits out a short list:

       12.34.56.789 - 3
      1.234.567.890 - 4

    It’s okay that the command spans multiple lines. Check those IPs and you might find your culprit.

    What ModSecurity Rule Hates Me

    I have a love/hate relationship with ModSecurity. My first WP post (not question) in the forums was about it. It’s great and protects things, especially when you tie it into IPTables and have it auto-ban people… Until you accidentally block your co-editor-in-chief. Whoops!

    For this one, you’ll need to ask the person impacted for their IPv4 address. Then you can run this:

    zgrep --no-filename IPADDRESS error.log*|grep --color -o "\[id [^]]*\].*\[msg [^]]*\]"|sort -h|uniq -c|sort -h
    

    That will loop through all the error logs (on DreamHost they’re in the same location as the access logs) and tell you what rules someone’s hitting. Then you can tweak the rules.

    Of course, if you’re not the root admin, you’ll want to ping your support reps with “Hey, found this, can you help?” They usually will.

    Don’t feel bad about this, and don’t blame the reps for this. ModSecurity is constantly changing, because jerks are constantly trying to screw with your site for funzies and profit (I guess). Every decent host out there is hammering the heck out of their rules constantly. They update and tweak and change. Sometimes when they do that, it reveals that a rule is too restrictive. Happens all the time.

    Long Running Requests

    Another cool thing is “What’s making my site slow” comes from “What processes are taking too long.”

    awk  '{print $10,$7}' access.log | grep -ivE '(.gif|.jpg|.jpeg|.png|.css|.js)'  | awk '{secs=0.000001*$1;req=$2;printf("%.2f minutes req time for %s\n", secs / 60,req )}' | sort -rn | head -50
    
    

    That gets me the 25 top URLs. For me it happened to list MP4s so I added that into my little exclusion list where .gif etc are listed.

    Who’s Referring?

    A referrer is basically asking “What site sent people here.”

    awk '{print $11}' access.log | \
     grep -vE "(^"-"$|/www.$host|/$host)" | \
     sort | uniq -c | sort -rn | head -25
    

    This one is a little weird to look at:

      15999 "-"
         31 "www.google.com"
          8 "example.com"
          4 "binance.com"

    The ‘example.com’ means “People came here from here” which always confuses me. More impressive is that top one. It means “People came here directly.” Except I know I’m using Nginx as a proxy, so that’s likely to be a little wonky.

    What are your favourite cli tools?

    Do you have one? Drop a line in the comments! (Be wary about posting code, it can get weird in comments).

  • Updating Multiple Posts’ Meta

    Updating Multiple Posts’ Meta

    I had 328 posts that I needed to add a meta field value to. Thankfully they all had the same value at the moment, so what I really needed was to tell WP “For all posts in the custom post type of ‘show’ add the post_meta key of ‘tvtype’ with a value of ‘tvshow’.”

    That sounded simple. It wasn’t.

    Googling “Updating multiple posts” or “Bulk Update Multiple Posts” (with WordPress in there) was frustratingly vague and told me to do things like the bulk edit from the post lists page. Well. Sure. If I added my post meta to the bulk editor (which I do know how to do) and felt like updating them 20 shows at a time, I could do that. Heck, I could make my page list 50 and not 20, and do it in 5 ‘cycles.’

    But that wasn’t what I wanted to do. No, I wanted to figure out how to do it faster forever, so that if I had to update 32,800 posts, I could do it in the least CPU intensive way.

    PHP

    If I was to do this in PHP, it would look like this:

    $ids = array(
    	'post_type' 	=> 'post_type_shows',
    	'numberposts'	=> -1,
    	'post_status'	=> array('publish', 'pending', 'draft', 'future'),
    );
    
    
    foreach ($ids as $id){
        add_post_meta( $id, 'tvtype', 'tvshow' );
    }
    

    I picked add_post_meta instead of update_ because while the update will add the meta if it’s not found, I didn’t want to update any of the shows I’d manually fiddled with already. And to run this, I’d have to put it in an MU plugin and delete it when I was done.

    Which… Yes. That could work. I’d want to wrap it around a user capability check to make sure it didn’t run indefinitely, but it would work.

    WP-CLI

    Doesn’t a nice command line call sound better, though? Spoiler alert: It’s not.

    I knew I could get a list of the IDs with this:

    $ wp post list --post_type=post_type_shows --fields=ID --format=ids
    

    That gave me a space-separated list

    And I knew I could add the meta like this for each show:

    $ wp post meta add 123 tvtype tvshow
    

    But who wants to do that 328 times?

    The documentation for wp post meta update said “Update a meta field.” A. Singular. Now it was possible that this could be for multiple posts, since the information on wp post update said “Update one or more posts” and “one or more” means one or more. But the example only had this:

    $ wp post update 123 --post_name=something --post_status=draft
    

    Notice how there’s no mention of how one might handle multiple posts? In light of clear documentation, I checked what the code was doing. For the update function, I found this:

    	public function update( $args, $assoc_args ) {
    		foreach( $args as $key => $arg ) {
    			if ( is_numeric( $arg ) ) {
    				continue;
    			}
    

    The check for if ( is_numeric( $arg ) ) is the magic there. It says “If this is an ID, keep going.” And no spaces. So the answer to “How do I update multiple posts?” is this:

    $ wp post update 123 124 125 --post_name=something --post_status=draft
    

    Great! So can I do that with post meta? Would this work?

    $ wp post meta add 123 124 125 tvtype tvshow
    

    Answer: No.

    So I took that list, used search/replace to turn it into 328 separate commands, and pasted them in (in 50 line chunks) to my terminal to update everything.

    Yaaaay.

  • Changing Git History

    Changing Git History

    Working on a group project in Git, I did the smart thing with my code. I made a branch and proceeded to edit my files. I also did a dumb thing. I made four commits.

    The first was for the first, ugly, functional version of the code. The second was a less ugly, kind of broken version. The third was the rewrite and the fourth was the working version. When I wanted to submit my changes for a review, it was going to be ugly. I did not need or want people looking at four commits They only wanted the one.

    Now I’m a weird person for how I do commits. I add a new feature like a new function to parse things, and I commit that. Then I change my CSS and commit that. And so on and so on. This means I can look through my commit history and see exactly when I made a change. When I’m ready to do my release, I document all the changes based on that commit log and have it as my message.

    But when you’re working with a team, and all they want is one clean commit? Well I’m their worst nightmare. There is a cure for this, though! You can squash your commits, merging them all into one.

    Squash

    Actually it’s rebase. It can be squash too, though. I ran the following command which says to rebase my last 4 commits:

    git rebase -i HEAD~4
    

    That opens up another editor

    pick b17617p Crap I need to do this thing!
    pick 122hdla Added feature HUMAN to autogenerate a humans.txt file
    pick nw9v88a Changed comment avatar size to 96px
    pick 8jsdy1m Updated CSS for comment avatars to make them a circle
    
    # Rebase b17617p..8jsdy1m onto b17617p
    #
    # Commands:
    #  p, pick = use commit
    #  r, reword = use commit, but edit the commit message
    #  e, edit = use commit, but stop for amending
    #  s, squash = use commit, but meld into previous commit
    #  f, fixup = like "squash", but discard this commit's log message
    #  x, exec = run command (the rest of the line) using shell
    #
    # If you remove a line here THAT COMMIT WILL BE LOST.
    # However, if you remove everything, the rebase will be aborted.
    #
    

    Now here’s where it’s weird. The first one, b17617p is the one I have to merge everything into. And it has the worst commit message, doesn’t it? Oh and I was totally not using the right formatting for how the company wants me to format my commits. They want the comment to be “Feature: Change” so I would have “Humans: Added new feature to autogenerate humans.txt”

    Since I knew I wanted to merge it all and totally rewrite the commit, I just did this:

    pick b17617p Crap I need to do this thing!
    squash 122hdla Added feature HUMAN to autogenerate a humans.txt file
    squash nw9v88a Changed comment avatar size to 96px
    squash 8jsdy1m Updated CSS for comment avatars to make them a circle
    

    Which, once saved and exited, gave me this:

    # This is a combination of 4 commits.
    # The first commit's message is:
    
    Crap I need to do this thing!
    
    # This is the 2nd commit message:
    
    Added feature HUMAN to autogenerate a humans.txt file
    
    # This is the 3rd commit message:
    
    Changed comment avatar size to 96px
    
    # This is the 4th commit message:
    
    Updated CSS for comment avatars to make them a circle
    
    # Please enter the commit message for your changes. Lines starting
    # with '#' will be ignored, and an empty message aborts the commit.
    # Explicit paths specified without -i nor -o; assuming --only paths...
    # Not currently on any branch.
    # Changes to be committed:
    #   (use "git reset HEAD <file>..." to unstage)
    #
    #	new file:   LICENSE
    #	modified:   README.textile
    #	modified:   Rakefile
    #	modified:   bin/jekyll
    #
    

    Since everything with a # is ignored, I deleted it and made it this:

    Humans: New Feature -- Humans.txt is now autogenerated
    Comments: Changed avatar size to 96px and edited CSS to make it a circle
    

    Yeah, that’s it. Admittedly, these should be two separate changes, but they’re all a part of the same project in this case so it’s okay.

    Of course, at the end of this, I looked at my code on our web tool and swore, because I’d left a debug line in. My hero Mike said “Don’t worry! ammend!”

    I made my change, instead of a normal git commit -a -m "These are my changes" I ran a git add FILENAME and git commit --ammend to fix up your most recent commit.

    It lets you combine staged changes with the previous commit instead of committing it as an entirely new snapshot. It can also be used to simply edit the previous commit message without changing its snapshot.

    And yes, it’s pretty awesome. Use it wisely.

  • Making a WP-CLI Plugin

    Making a WP-CLI Plugin

    I love wp-cli. It makes my life so much easier in so many ways.

    I’ve added in some basic commands to some of my plugins because it makes things easier for others. And as it happens, adding wp-cli commands to your plugins isn’t actually all that hard. But did you know you don’t have to?

    The Basic Code

    For our example I’m going to make a command called halfelf and it’s going to have a sub-command called ‘stats’ which outputs that the HalfElf is alive. And that looks like this:

    WP_CLI::add_command( 'halfelf', 'HalfElf_CLI' );
    
    class HalfElf_CLI extends WP_CLI_Command {
    
    	/**
    	 * Get HalfElf Stats
    	 *
    	 * ## EXAMPLES
    	 *
    	 * wp halfelf stats
    	 *
    	 */
    	public function stats( ) {
    		WP_CLI::success( __( 'HalfElf is currently alive.', 'halfelf' ) );
            }
    }
    

    The docblock controls the output for the help screen, which is possibly the most brilliant thing about it.

    Of course, all that does is make a command that doesn’t rely on any WordPress plugin, which is cool. Here’s a different example. In this one, I’m triggering a command to my logger function to reset the backup log or to view it:

    
    	/**
    	 * See Log Output
    	 *
    	 * ## OPTIONS
    	 *
    	 * empty: Leave it empty to output the log
    	 * 
    	 * reset: Reset the log
    	 *
    	 * wp dreamobjects logs
    	 * wp dreamobjects logs reset
    	 *
    	 */
    
    	public function log( $args, $assoc_args  ) {
    		if ( isset( $args[0] ) && 'reset' !== $args[0] ) {
    			WP_CLI::error( sprintf( __( '%s is not a valid command.', 'dreamobjects' ), $args[0] ) );
    		} elseif ( 'reset' == $args[0] ) {
    			DHDO::logger('reset');
    			WP_CLI::success( 'Backup log reset' );
    		} else {
    			file_get_contents( './log.txt' );
    		}
    	}
    

    Because wp-cli runs as WordPress, it has access to WordPress commands and functions.

    But. Where do we put this file?

    In a WordPress Plugin

    If you want to use that code in a WordPress plugin, you put it in a file called wp-cli.php (or whatever you want) and then call it in your main WordPress plugin file like this:

    if ( defined( 'WP_CLI' ) && WP_CLI ) {
    	require_once( 'wp-cli.php' );
    }
    

    If you do that, I would recommend putting this at the top of your wp-cli.php file.

    if (!defined('ABSPATH')) {
        die();
    }
    
    // Bail if WP-CLI is not present
    if ( !defined( 'WP_CLI' ) ) return;
    

    That will prevent people from calling the file directly or in other ways.

    In the case of my DHDO code, I added in this check:

    if( class_exists( 'DHDO' ) ) {
    	WP_CLI::add_command( 'dreamobjects', 'DreamObjects_Command' );
    }
    

    That means if (for whatever reason) DHDO can’t be found, it prevents you from doing things.

    On It’s Own

    But. That first command doesn’t need a plugin, does it? So if it doesn’t need a plugin, could I still use them? The answer is of course. The wp-cli.php file is the same, but where you put it changes. This will all be done in SSH. You could do it in SFTP if you wanted.

    In your home (or root) directory we have to make a folder called .wp-cli – please note the period, it’s like your .htaccess file. Except it’s a folder. In there you’re going to make a folder called commands and in that folder we’ll put another one for our commands. Like if I’m putting my HalfElf commands out there, I’d make a folder called halfelf and in there I put my command file command.php

    That gives me this: ~/.wp-cli/commands/halfelf/command.php

    However that doesn’t act like plugins or themes or mu-plugins, you have to add the command to another file. I know, I know. Make a file called config.yml and put it in .wp-cli so you have this: ~/.wp-cli/config.yml

    The content of that file is this:

    require:
      - commands/halfelf/command.php
      - commands/some-other/command.php
    

    And that’s it! Now you’ve got some command line tools for your WordPress site!

  • Self Ghosted

    Self Ghosted

    I had to restart everything.

    You see, getting Node.js and apache to work together wasn’t going to fly, so I went and built a new VPS with Ubuntu over on DreamHost. Obviously not everyone has this luxury, but since I do, and since DreamHost made this as easy as clicking on a box, I thought it would be perfect.

    Requirements

    You have to be shell savvy for this. Sorry. There’s no other way. Just accept that and move on.

    You need a VPS. The new Ubuntu SSD VPSes all have a one-click ability for Node.js which makes life easier.

    You should use nginx and not apache.

    Ghost requires Node.js 0.10.x (not 0.12.x). They recommend Node.js 0.10.36 & npm 2.5.0. Of course, DreamHost installs node 0.12.x and npm 2.5.x. You can check by typing node -v and npm -v to see what versions you have. Does this mean you can’t run Ghost? No, it’ll work, but it’s not what they like.

    You’ll also need an ‘admin’ account on your server. From panel, click on “VPS” and then “Manage Admin Users” to add a new user to that box.

    Get Node.js on your VPS

    This is quite simple. First, get an Ubuntu VPS. All the new boxes on DreamHost are Ubuntu, so this is the easy part. Once you have the VPS on DreamHost, make a new domain. I made ghost.elftest.net and in the settings, I checked a box to use node.js:

    Enabling Node on DreamHost is Easy - click a box

    That will force my file path to be ~/ghost.elftest.net/public (normally on DreamHost it’s ~/domain.com you see) but that’s not an issue since this is a new box. That path will be my ‘home’ folder for the domain and where I install everything (if someone asks you ‘where is X installed on your server?’ you tell them that path). That path also means that had I already been using that domain, it would mess up my folders and paths. If you’re doing this on an existing domain, keep that in mind.

    Install Ghost

    Like I said before, you have to install on your server, not your desktop. Every single doc I read told me I HAD to do this as the server admin, someone with sudo access. I really, really, really did not like that. I mean, epic levels of hate for the idea that I couldn’t have my user account own things, and have the code in my darn web folder.

    Me being me, I hatched a plan. I was just going to do it. I was just going to install it in my webfolder and see how it goes. I came up with this theory because when I read How To Install Ghost on DreamHost I noticed it wanted me to have a specific user chown the folders. Not the admin user, but a special ghost user. So what if I just made ‘elftestghost’ the ghost user, and what if I just used ~/ghost.elftest.net/public/ghost/ as my install directory instead of /var/www/ghost/? And this way, I’d be able to pop in and edit things as my normal user.

    So here’s what I did.

    $ curl  -L https://ghost.org/zip/ghost-latest.zip -o ghost.zip
    $ unzip -uo ghost.zip -d ghost
    $ cd ghost/
    $ npm install --production
    

    That’s it. Ghost is installed. I expected that last step to fail and I’m still a little surprised it didn’t. If it does fail for you, run it as your admin account with sudo, and then chown it over to your ‘owner.’

    Configure Ghost

    Now that Ghost is installed, you may wonder where it is. We haven’t finished the install yet as it happens. We need to configure it and to do that, we copy the file config.example.js to config.js and open it up.

    Once you look at it you see that there are multiple ‘config’ options. Since we called --production in our start command, logically we’re going to edit that section. And lo, there’s the bad URL:

    config = {
        // ### Production
        // When running Ghost in the wild, use the production environment
        // Configure your URL and mail settings here
        production: {
            url: 'http://my-ghost-blog.com',
            mail: {},
    

    Change that to the right URL. Mine is http://ghost.elftest.net

    Also look for this:

            server: {
                // Host to be passed to node's `net.Server#listen()`
                host: '127.0.0.1',
                // Port to be passed to node's `net.Server#listen()`, for iisnode set this to `process.env.PORT`
                port: '2368'
            }
    

    Change the host to 0.0.0.0 and save the file. You can also set it to the IP of your server, but as that might change, it’s not required.

    Finally you’ll want to set up mail which can be complex. I set up a special account on my own server.

    Setup Ghost

    Seriously this is not a five minute install. Your site starts with a Hello World type post which tells you all about the glory of Markdown. If you go to your admin page (http://ghost.elftest.net/ghost/) it sends you to http://ghost.elftest.net/ghost/setup/ where you get this:

    Ghost: Create User

    This actually makes more sense than it did on the Ghost Pro site, where I was wondering why I had to make a user and then a user. Here I’m clearly making a user for the site. There I was making a user for their network (Ghost Pro) and then one for my site.

    Setup Nginx Proxy

    Will it never end? The reason we want to do this, and this is why we’re on nginx and not apache, is in order to have a pretty URL without the port number.

    Log in to your server as your ‘normal’ account (the one who owns the domain, not the admin) and make an nginx folder for config:

    cd ~
    mkdir -p nginx/ghost.elftest.net
    

    Then make a file called ghost.conf with the following:

    location / {
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   Host      $http_host;
        proxy_pass         http://localhost:2368;
    }
    

    Restart nginx (which yes, you need the sudo power for so yes, you need an admin account) and it’ll magically work. If you get a “502 Bad Gateway” error, it’s because Ghost isn’t running. Speaking of…

    Make Ghost Always Start

    Okay so I actually do need my admin account. I went with the init script approach to keeping the Ghost always on.

    $ sudo curl https://raw.githubusercontent.com/TryGhost/Ghost-Config/master/init.d/ghost \
      -o /etc/init.d/ghost-elftest
    

    Then I edited the ghost-elftest file:

    GHOST_ROOT=/var/www/ghost
    GHOST_GROUP=ghost
    GHOST_USER=ghost
    

    I changed that to where ghost was really installed, but it brings up an interesting thought. What if I wanted multiple ghost instances? Well that’s actually why I named the file ‘ghost-elftest’ instead of ‘ghost’ (which they recommend). With this setup, I can name an init file for each instance and run the commands as sudo ghost-elftest start and so on. Keep in mind, you also have to pick a custom port for each instance.

    There are three final commands to run in order to force Ghost to start up on reboot:

    $ sudo chmod 755 /etc/init.d/ghost-elftest
    $ sudo update-rc.d ghost-elftest defaults
    $ sudo update-rc.d ghost-elftest enable
    

    Now I can use Ghost to my heart’s content.

    Conclusion?

    A WordPress killer this is not. It’s just not. If they can make it run without the need for admin/root/sudo access, it has a chance. Once it’s set up, it’s quite nice, but the 5 minute install this is not, and it’s going to need that to beat the beast.

  • Migrating A WordPress Site With wp-cli

    Migrating A WordPress Site With wp-cli

    This is … crazy simple. I wanted to move a site from ipstenu.org to ipstenu.com (yes, I own that too). While ipstenu.org is a Multisite network, ipstenu.com is where I put a ton of add-on domains. I was moving a site over and, as it was WordPress, did it in a matter of minutes.

    Add the domain to the … domain

    Since I don’t care to have multiple hosting accounts, and I’m the only one with SSH/FTP access, it’s safe enough for me to do this. There is a risk when you share multiple domains in one hosting account, that if one gets hacked they’re all vulnerable, but I consider it low in my situation. Every plugin is vetted, every file is checked, and then I went and gave each add-on it’s own FTP account. Neurotic? Thy name is me.

    Anyway, I add the new domain to my hosting where I want it.

    Create the new DB

    I have to make a new database, and generally a new DB user, on the server too.

    Export!

    On existing hosting, I do this:

    wp db export
    

    That gives me my SQL file, thanks to WP-CLI. It’ll be named example_com.sql and will sit in my folder with .htaccess and everything else.

    Copy!

    I do it via SSH. I go to the new location and run this:

    scp -r olduser@example.com:path/to/files/ .
    

    Since I have ssh keys set up, it’s easy. If I don’t, I’ll put in the password, but that’s straightforward.

    Edit wp-config.php

    Now I have to point to the new DB. Sometimes I name it the same, but usually I don’t, so I’ll edit the DB name, DB user, and password.

    Import the old DB

    Ready?

    wp db import example_com.sql
    

    Boom. It’s all dumped in! Only two steps left!

    Search & Replace

    I love this one.

    wp search-replace example.com newexample.com --dry-run
    

    I ALWAYS dryrun test it. This is a serialization safe search, so rarely is it ever going to be an issue to just run, but it lets me make sure I don’t get any wonky results. I never have, so re-run without dry-run.

    Cleanup!

    Delete the SQL file, delete the old files on the old server.

    Drink

    El Chorro Margarita

    I moved code. That’s shipping, right? Or is it margaritas are for migrations?