This is a guide how to upgrade your JIRA server. JIRA has security support for two years from a point release. (In this document, a "point release" refers to the minor release.) This information is based on the official Atlassian documentation.

I want to point out that this is for personal reference, and it's based on my own (very idiosyncratic) configuration, so take the specifics with a grain of salt. The general procedure should be simple to follow.

Step 0: Locate relevant details.

You might need to know the administrator password to JIRA for what follows. Make a note of your license key, it's in Configure > Applications.

Step 1: Find out what version of JIRA you are using.

You can see this at the bottom of the JIRA page once you have logged in. In my case it reads:

Atlassian JIRA Project Management Software (v7.7.0#77001-sha1:3be3151)

Step 2: Back up the database.

Dump the database using postgres tools.

pg_dump -F c -Z 9 jira > jira.backup

Step 3: Back up the files.

The easiest method here is just to tar up your entire JIRA directory and dump it somewhere, if it's feasible. I just run this:

root@jiraserver # tar -cf ~/jira-old.tar /jira

My directory clocked in at a nifty 1.3GB of data.

Step 4: Find the archive for the new version.

You can find this at https://www.atlassian.com/software/jira/update.

Click View all versions and it will open a lightbox.

This dialogue lets you select the exact version you want. Click the dropdown at the bottom left and switch to 7 -- this is your choice of major version.

Now it will list the minor versions available. Select 7.7.

Now it will list the patch versions. Select 7.7.4, the highest one available. Click Choose this version.

You'll see several options for the format of the download. The best version for upgrading is one of the archives, which are installed using the 'manual' process.

So you should choose TAR.GZ Archive, and click Get started.

Agree to the license agreement.

This will provide you with a file named atlassian-jira-software-7.7.4.tar.gz, save this file.

Step 5: Stop JIRA

JIRA should be managed with systemd at the moment, so this should be as simple as systemctl stop jira.

Step 6: Determine how to replace files

In my configuration, JIRA houses its static files under /jira/install. So that's the directory you should be replacing. When you extract the tar, you'll get a new subdirectory: atlassian-jira-software-7.7.4-standalone/. So in my example, you'd be doing the following:

root@myserver # cd /jira
root@myserver # tar -xzf ~/atlassian-jira-software-7.7.4.tar.gz
root@myserver # mv install install.old
root@myserver # mv atlassian-jira-software-7.7.4-standalone install

Step 7: Apply Puppet manifests

I assume that you control the configuration of server.xml and other JIRA configuration files in Puppet. You'll want to reapply these. The way that you do this varies depending on your setup. In a good world, you can just run puppet agent --test. I use a masterless setup, so my command looks something more like this:

root@myserver # cd ~/mypuppetrepo
root@myserver # /opt/puppetlabs/bin/puppet apply --verbose --no-splay --modulepath ./modules masterless/myserver.pp

Once the changes are applied, you have to make one more change.

Step 8: Apply JVM-specific tweak

Add this line to the top of /jira/install/bin/catalina.sh:

JRE_HOME=/jira/jvm/jdk1.8.0_162

You're probably wondering, why do this? Actually, the best option is to set this in the environment. Or, an even better option would be to set it in the systemd service file, something akin to:

[Service]
...
Environment=JAVA_HOME=/jira/jvm/jdk1.8.0_162

This could more easily be puppeted.

Step 9: Fix permissions on the writable directory

chown jira:jira /jira/install/logs
chmod 0755 /jira/install/logs

chown jira:jira /jira/install/work
chmod 0755 /jira/install/work

This could (and should) be puppeted. In addition, these directories should probably be configured to be outside of the /jira/install tree, in a sort of /var-like arrangement.

Step 10: Fix the references to the JIRA home directory

Modify /jira/install/atlassian-jira/WEB-INF/classes/jira-application.properties

It should contain:

jira.home=/jira/data

This can also be specified in the systemd service file, although it isn't at present.

Step 11: Start JIRA

And monitor /jira/install/logs/catalina.out file. If something goes wrong, bear in mind that you might have to manually stop and restart JIRA using the scripts under /jira/install/bin, because it will try to hang around even if it wasn't able to start correctly, which can confuse you.

Posted 2019-03-28

The goal: keep your database authentication details out of your Git repository.

The mechanism: environ and the lein-environ plugin.

How to do it: firstly, your dependencies should look like so:

:dependencies [[org.clojure/clojure "1.8.0"]
               [environ "1.1.0"]]
:plugins [[lein-environ "1.1.0"]]

You need both the dependency and the plugin.

To just specify a static value that always gets passed through to the code, just specify it directly in the project.clj:

:env {:daves-variable "42"}

To separate this out, you create a local profiles.clj file in your project root. You can add this to gitignore.

{:dev {:env {:database-password "xyzzy"}}}

Now from your code, you just do the following:

(ns myapp.core
  (:require [environ.core :as environ]))

(defn main []
  (println (:database-password environ/env)))

The namespace environ.core exports a var env which is a configuration map. Keys are automatically kebab cased, and Leiningen knows to merge the profiles.clj from the project root into the environment, due to the lein-environ plugin.

Posted 2019-03-25

Some might be confused by the changes to Fabric in 2.x. It's a complete rewrite and a complete API redesign.

Upload binary data as file-like object

c = fabric.Connection('visarend.solasistim.net')
f = io.BytesIO(b"some initial binary data: \x00\x01")
c.put(f, 'out.bin')

Use plaintext authentication details

password = 'xyzzy'
connect_kwargs = {'password': password}
c = fabric.Connection('visarend.solasistim.net', connect_kwargs=connect_kwargs)
c.run('/bin/true')

Bundled sudo operations as part of file transfer

In the words of the Fabric developers,

This was one of the absolute buggiest parts of v1 and never truly did anything users could not do themselves with a followup call to sudo, so we opted not to port it.

This mouthful will do it:

import hashlib
import fabric
import shlex

class RemoteTemporaryPath(object):
    # public name attribute designed to be retrieved by user
    name = None

    def __init__(self, c, remote_path):
        hasher = hashlib.sha1()
        hasher.update(c.host.encode('utf-8'))
        hasher.update(remote_path.encode('utf-8'))
        self.name = hasher.hexdigest()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        c.run("rm -rf {}".format(shlex.quote(self.name)))

def put_with_sudo(c, local_path, remote_path):
    with RemoteTemporaryPath(c, local_path) as r:
        c.put(local_path, r.name)
        c.sudo("mv {} {}".format(shlex.quote(r.name), shlex.quote(remote_path)), password='xyzzy')

c = fabric.Connection('visarend.solasistim.net')
put_with_sudo(c, "foo.py", "/etc/foo.py")

A very important thing here: you need to decide at script time whether you will respond to the sudo authentication prompt by hand or if you will use some other method to get the sudo password. Here we are taking the approach of "use some other method". If you need to respond to the sudo prompt by hand, you need to use c.run("sudo foo") instead of c.sudo("foo", password='bar'). NB: I don't claim this code is 100% safe, please be careful.

Posted 2019-03-05

You face a problem where you want to remove a leading 'container' directory from an archive when extracting. This can happen when you're using the Puppet archive resource from puppetlabs-archive module.

archive { '/tmp/somearchive.zip':
    extract => true,
    extract_path => '/srv/http/omeka_s',
    source => 'https://github.com/omeka/omeka-s/releases/download/v1.3.0/omeka-s-1.3.0.zip'
}

Here we want to extract directly into /srv/http/omeka_s so that the index.php is created inside that directory. Problem is that the file contains an embedded directory, omeka-s. Note that it doesn't quite match our desired one, so we can't do the trick of extracting to /srv/http and relying on the archive structure to create the desired directories. [That's probably wise, as that approach is somewhat fragile.]

If it was a tar file, we could use tar --strip-components=1, but as it's a zip file we can't do this. Look on the net and you'll find many asking this question. It's 2019 and we still don't have a good solution for extracting archives without caring about the format. Well, there are two...

dtrx -- "do the right extraction" works well, except it's designed for the opposite of this situation. It's designed for avoiding tarbombs. In fact, we have a non-tarbomb when we actually desire a tarbomb. Still, it's worth a mention as it's packaged for many distributions.

7zip doesn't work because it does the equivalent of -j when using the -r option.

So here's my hacky python workaround. I was really hoping not to have to do this, and to be able to recommend a solution that was already packaged in Debian, but I couldn't find one, so this will have to do for now.

#! /usr/bin/env python3

import sys
import zipfile
import os
import shutil

strip_n = int(sys.argv[1])
zipfile_path = sys.argv[2]

with zipfile.ZipFile(zipfile_path) as the_zip:
    namelist = the_zip.namelist()

    for member in namelist:
        fixed_path = os.path.normpath(member)
        components = fixed_path.split(os.sep)

        if len(components) < strip_n:
            raise Exception('unexpected number of components in filename')

        stripped = components[strip_n:]
        target_path = os.path.join(*stripped)

        upperdirs = os.path.dirname(target_path)
        if upperdirs and not os.path.exists(upperdirs):
            os.makedirs(upperdirs)

        with the_zip.open(member) as source, open(target_path, "wb") as target:
            shutil.copyfileobj(source, target)
Posted 2019-02-28

The first thing to do is to create your combo box. If you try to do the simple zero argument constructor, you're going to get a box with zero choices, a blank dropdown. This is what happens when you use the QStandardItemModel, which is empty by default. You can choose either to use the QStandardItemModel or create your own subclass of QAbstractItemModel.

QComboBox* comboBox = new QComboBox;

Remember that a combo box represents a choice between several items of a list. So you need some way to specify that list. You have a choice of abstraction levels here. The simplest way is to use the QStandardItemModel directly, via the convenience methods provided by QComboBox. In order of complexity your other options are.

  • Use QStringListModel directly
  • Subclass QStringListModel
  • Subclass QAbstractListModel
  • Subclass QAbstractItemModel

Using QStringModel directly is simple.

QStringList list = {"foo", "bar", "baz"};
QAbstractItemModel* myModel = new QStringListModel(list, comboBox);
comboBox->setModel(myModel);

What if we want to set the selected item?

comboBox->setCurrentIndex(1);

This will set the box to select bar, given the model above. That is, indices are zero-based.

Posted 2019-02-27

The puppet agent is not available in the base repository for the CentOS 7 release.

Puppet 3.6.2 is available in the EPEL repository. This is somewhat painfully old. Puppet 5.5.10 will debut in Debian Buster. The IUS repository does not feature packages for this either.

yum install ruby will give you Ruby 2.0.0.648 by default.

Install in this order; facter; hiera; puppet. It will all go to /usr, for some reason that is a mystery to me.

Test it out:

[amoe@localhost bin]$ puppet --version
4.8.2

Check that we can access our puppet server.

[amoe@localhost bin]$ ping -c 1 puppet.solasistim.net
PING kupukupu.solasistim.net (92.26.198.147) 56(84) bytes of data.
64 bytes from host-92-26-198-147.as13285.net (92.26.198.147): icmp_seq=1 ttl=63 time=29.7 ms

--- kupukupu.solasistim.net ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 29.778/29.778/29.778/0.000 ms

Modify your hostname to something sensible, /etc/hostname. Also add an alias to the hostname into /etc/hosts.

Most files will install to /opt/puppetlabs by default.

Also install bind-utils for the host program that is required by Facter.

Posted 2019-02-25

These are my endgame saves from X3 Reunion, Terran Conflict, and Albion Prelude.

Posted 2019-02-02

This is an example of a technique that's shown in the Vue manual, basically you maintain two pieces of state, use one as the backing store and use a watcher to update the derived state using tweening mechanisms such as TweenLite in this case. Shown with a few hacks that are necessary to get it working with the d3-cluster node representation.

<template>
  <div>
    <svg viewBox="0 0 1024 768" width="1024" height="768" 
         xmlns="http://www.w3.org/2000/svg">
      <circle v-for="descendant in tweenedDescendants"
              :cx="descendant.x"
              :cy="descendant.y"
              r="10"
              stroke="red" fill="grey"/>
    </svg>

    <button v-on:click="relayout">Relayout</button>
  </div>
</template>

<script lang="ts">
import Vue from 'vue';
import {hierarchy, cluster, HierarchyNode} from 'd3';
import _ from 'lodash';
import {TweenLite} from 'gsap';

const familyData = {
    "name": "Eve",
    "children": [
        {
            "name": "Cain"
        },
        {
            "name": "Seth",
            "children": [
                {
                    "name": "Enos"
                },
                {
                    "name": "Noam"
                }
            ]
        },
        {
            "name": "Abel"
        },
        {
            "name": "Awan",
            "children": [
                {
                    "name": "Enoch"
                }
            ]
        },
        {
            "name": "Azura"
        }
    ]
};


interface FamilyDatum {
};

function initialCluster(val: any): HierarchyNode<FamilyDatum> {
    const theHierarchy = hierarchy(val);
    const clusterLayout = cluster();
    clusterLayout.size([1024, 768]);
    clusterLayout(theHierarchy);
    return theHierarchy;
}

export default Vue.extend({
    data() {

        return {
            hierarchyRoot: initialCluster(familyData) as HierarchyNode<FamilyDatum>,
            tweenedHierarchyRoot: initialCluster(familyData) as HierarchyNode<FamilyDatum>
        };
    },
    mounted() {
        window.setInterval(this.relayout, 1000);
    },
    methods: {
        relayout() {
            const clusterLayout = cluster();

            // these hacks necessary because cluster() itself does some array
            // mutations that fool the reactivity
            const clonedHierarchy = _.clone(this.hierarchyRoot);
            clusterLayout.size([Math.random() * 1024, Math.random() * 768]);
            clusterLayout(clonedHierarchy);

            this.hierarchyRoot = clonedHierarchy;
        },
    },
    computed: {
        descendants(): HierarchyNode<FamilyDatum>[] {
            return this.hierarchyRoot.descendants();
        },
        tweenedDescendants(): HierarchyNode<FamilyDatum>[] {
            return this.tweenedHierarchyRoot.descendants();
        },
    },
    watch: {
        // No deep watch needed because we always reassign the whole
        // array
        hierarchyRoot(val, oldVal) {
            console.log("inside handler function");
            // We know that descendants() function returns references to
            // the nodes that can be directly modified by mutation.
            // Because the x and y values have already been declared in the
            // data() method  -- that is, cluster() was already called once
            // to add the properties -- Vue knows that it should react to changes
            // on these properties.

            const targetDescendants = val.descendants();
            const tweenedDescendants = this.tweenedHierarchyRoot.descendants();

            for (var i = 0; i < tweenedDescendants.length; i++) {
                const node = tweenedDescendants[i];

                const targetX = targetDescendants[i].x;
                const targetY = targetDescendants[i].y;

                TweenLite.to(node, 0.5, {x: targetX, y: targetY});
            }
        }
    }
});
</script>

<elyts lang="less">
</style>

This is based on a technique from the Vue manual.

Posted 2019-01-06

The basic procedure is as follows: You create an interface RootState that defines your fields. You probably already have types for these various data items. You may have been previously forced to cast your bottom values to avoid ending up as never[], etc. So create an interface representing these values. Then refactor your store to type-check correctly when the object is assigned to a variable fo type StoreOptions<RootState>.

Then you derive another type as such.

import { Module } from 'vuex';

// ...

const graphView: Module<GraphViewState, RootState> = {
   // ...
}

Move all the previous contents of RootState into GraphViewState (in this example). RootState itself should become an empty interface.

Now you can include this in the modules key when constructing your Vuex store.

const store: StoreOptions<RootState> = {
    modules: { graphView }
};

export default new Vuex.Store(store);
Posted 2018-12-23

Here is a simple component for Vue/TS that demonstrates using the transition-group directive to collapse the width of elements when they are removed from a list.

One catch is that an unlabelled 'div' is inserted as the parent of all list items. This can cause problems when styling things using flexbox, which treats only its direct children as layout items. Also, if you have styling applied to the children, applying styling to the transition classes won't override those styles, because the transition style may be less specific than the styles on the child elements.

<template>
  <div class="home">

    <div class="container">
      <transition-group name="mytrans" tag="div">
        <div v-for="(box, index) in boxes"
             v-if="box.isVisible"
             :key="box.id"
             class="box">
          {{box.id}}
          <button v-on:click="hide(index)">Hide</button>
        </div>
      </transition-group>
    </div>
  </div>
</template>

<script lang="ts">
import Vue from 'vue';
import _ from 'lodash';

interface Box {
    isVisible: boolean;
    id: number;
}


export default Vue.extend({
    name: 'home',
    data() {
        return {
            boxes: [] as Box[]
        };
    },
    components: {},
    created() {
        for (var i = 0; i < 5; i++) {
            this.boxes.push({isVisible: true, id: i});
        }
    },
    methods: {
        hide(index: number): void {
            this.boxes[index].isVisible = false;
        }
    }
});
</script>

<elyts>
.container {
    height: 200px;
    background-color: blue;
}

/* Because vue transition creates a wrapper div around the transition'ed elements,
   we need to style that div to layout the elements correctly. */
.container > div {
    display: flex;
    flex-direction: row;
}

.box {
    height: 150px;
    width: 150px;
    margin: 16px;
    background-color: green;
}


.mytrans-leave-active {
    transition: all 0.5s ease-in;
}

.mytrans-leave-to {
    width: 0px;
}
</style>
Posted 2018-12-18
Emoji Representations
Posted 2018-09-14
Thoughts on Cheesesteak & More
Posted 2018-08-29
Vue + GraphQL + PostgreSQL
Posted 2018-07-20
Neo4j Cypher query to NetworkX
Posted 2018-05-09
FP & the 'Context Problem'
Posted 2018-02-27
Cloake Vegetable Biryani
Posted 2018-02-25
FFXII Builds
Posted 2018-02-02
Custom deployments solution
Posted 2017-12-09
SCons and Google Mock
Posted 2017-11-30
Sunday Lamb Aloo
Posted 2017-11-19
centos 6 debian lxc host
Posted 2017-11-03
Srichacha Noodle Soup
Posted 2017-10-17
Kaeng Kari
Posted 2017-10-13
Ayam Bakar (Sri Owen)
Posted 2017-10-12
Pangek Ikan (Sri Owen)
Posted 2017-10-12
Chicken Tikka Balti Masala
Posted 2017-10-06
Clojure Log Configuration
Posted 2017-09-28
Clojure Idioms: strict-get
Posted 2017-09-28
About
Posted 2017-09-18
Philly Cheesesteak
Posted 2017-09-14
Welcome
Posted 2017-09-13
Srichacha Kaeng Pa
Posted 2017-08-31
Malaidar Aloo
Posted 2017-08-10
BBQ Balti Chicken
Posted 2017-07-19
Sabzi Korma
Posted 2017-07-18
Vegetable Tikka Masala
Posted 2017-07-02
Soto Ayam
Posted 2017-06-08
Bombay Aloo w/Bunjarra
Posted 2017-06-03
Chicken Dopiaza
Posted 2017-06-01
LJ Bunjarra
Posted 2017-05-31
Glasgow Lamb Shoulder Tikka
Posted 2017-05-24
Tofu Char Kway Teow
Posted 2017-05-12
King Prawn Balti
Posted 2017-04-24
Ad-hoc Quorn Rogan Josh
Posted 2017-04-15
Glasgow Vindaloo
Posted 2017-03-28
Rempeyek
Posted 2017-03-26
Toombs Saag Balti
Posted 2017-02-25
Glasgow Bombay Rogan Josh
Posted 2017-02-21
Glasgow Chicken Balti
Posted 2017-02-16
Quorn Balti & Cloake Naan
Posted 2017-02-03
Two Spice Marinades
Posted 2017-01-18

This blog is powered by coffee and ikiwiki.